From 4012b777cf08f2e8dab6040f61944c04067a3fab Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sun, 5 Jul 2026 22:14:05 -0700 Subject: [PATCH 01/32] fix(types): stop var/const arrow hoisting placeholder from tripping TS2403 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HoistConstFunctionExpression pre-registers an unannotated var/const arrow's type before its body is checked, so forward references resolve. When neither the binding nor the arrow itself annotates a return type, it hoisted a `(params) => any` placeholder. VisitVar's TS2403 redeclaration check then compared that placeholder against the real inferred return type once the declaration's own initializer was checked — falsely reporting "subsequent variable declarations must have the same type" against the var's own first-and-only declaration whenever the real return type wasn't `any`. Register a bare `any` for this case instead, matching how HoistVarDeclarations/HoistClassDeclarations placeholder forward references — `any` is already exempt from the redeclaration check. --- TypeSystem/TypeChecker.Statements.Functions.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/TypeSystem/TypeChecker.Statements.Functions.cs b/TypeSystem/TypeChecker.Statements.Functions.cs index 67c26de3..31514f2a 100644 --- a/TypeSystem/TypeChecker.Statements.Functions.cs +++ b/TypeSystem/TypeChecker.Statements.Functions.cs @@ -339,9 +339,16 @@ private void HoistConstFunctionExpression(Token name, string? typeAnnotation, Ex } else { - // No return type specified - use Any for hoisting - // The actual type will be checked during normal processing - returnType = new TypeInfo.Any(); + // Neither the binding nor the arrow annotates a return type. A precise + // `(params) => any` placeholder here looks like a REAL prior declaration to + // VisitVar's TS2403 redeclaration check once the real (usually non-`any`) + // inferred return type is established for this var's OWN first-and-only + // declaration — falsely tripping "subsequent variable declarations must have + // the same type". Register a bare `any` instead, matching how + // HoistVarDeclarations/HoistClassDeclarations placeholder forward references — + // `any` is already exempt from that check. + _environment.Define(name.Lexeme, new TypeInfo.Any()); + return; } // Handle 'this' type From 18d8c67a77166e5e226a33b8ddfec2835dbefe79 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sun, 5 Jul 2026 22:14:06 -0700 Subject: [PATCH 02/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20subtypingWithCallSignatures2/3/4=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 219 Pass / 59 Fail (was 216/62), 0 regressions. --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index f3304492..8eae2cc1 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -255,9 +255,9 @@ tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOf tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithRecursiveConstraints.ts Fail tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfUnion.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures.ts Pass -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures2.ts Fail -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures3.ts Fail -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures4.ts Fail +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures2.ts Pass +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures3.ts Pass +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures4.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignaturesA.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignaturesWithOptionalParameters.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignaturesWithRestParameters.ts Pass From fd8912038b25ca7c7398ad7649a168dc9f5ce9b0 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sun, 5 Jul 2026 22:54:19 -0700 Subject: [PATCH 03/32] fix(types): fix swapped-branch bug in ternary common-type selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CheckTernary picked the wider of the two branch types via IsCompatible(thenType, elseType) || IsCompatible(elseType, thenType), but returned `thenType` unconditionally regardless of which clause fired. When elseType was actually the wider type (thenType assignable to elseType), the narrower thenType was returned instead — producing a different, order- dependent result for `cond ? a : b` vs `cond ? b : a`, which should be identical since both simply pick the common type of the same two operands. Same "pick-the-wider-of-two-types with a swapped return" anti-pattern already found and fixed in CheckLogicalAssign; this is a second, independent sibling instance in the ternary operator. --- TypeSystem/TypeChecker.Operators.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/TypeSystem/TypeChecker.Operators.cs b/TypeSystem/TypeChecker.Operators.cs index 7e623170..ffa49ee3 100644 --- a/TypeSystem/TypeChecker.Operators.cs +++ b/TypeSystem/TypeChecker.Operators.cs @@ -393,11 +393,16 @@ private TypeInfo CheckTernary(Expr.Ternary ternary, TypeInfo? contextualType = n elseType = CheckExprWithContext(ternary.ElseBranch, contextualType); } - // Return the more specific type, or thenType if both are compatible - if (IsCompatible(thenType, elseType) || IsCompatible(elseType, thenType)) + // Return whichever branch type is the wider of the two (the other is assignable to it), + // or thenType if they're mutually compatible (e.g. identical types). + if (IsCompatible(thenType, elseType)) { return thenType; } + if (IsCompatible(elseType, thenType)) + { + return elseType; + } // Return union of both branch types return new TypeInfo.Union([thenType, elseType]); From 9330164770d8d6b94f56b3bd6d50d9d130c146fc Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sun, 5 Jul 2026 22:54:19 -0700 Subject: [PATCH 04/32] fix(types): relate namespace/module types structurally in IsCompatibleCore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TypeInfo.Namespace and TypeInfo.Module had no dedicated case anywhere in IsCompatibleCore's decision tree, so a namespace-or-module-typed value fell through to the final `return false` — making it spuriously incompatible with every target, including structurally identical (or literally the same) namespace/module type. Relate them the same way object/record types are related: every member the expected type exposes must exist on the actual type with a compatible type. --- TypeSystem/TypeChecker.Compatibility.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/TypeSystem/TypeChecker.Compatibility.cs b/TypeSystem/TypeChecker.Compatibility.cs index 3c591fdb..2457dbfb 100644 --- a/TypeSystem/TypeChecker.Compatibility.cs +++ b/TypeSystem/TypeChecker.Compatibility.cs @@ -422,6 +422,23 @@ private bool IsCompatibleCore(TypeInfo expected, TypeInfo actual) if (!_strictNullChecks && actual is TypeInfo.Null or TypeInfo.Undefined) return expected is not TypeInfo.Never; + // Namespace/module types (typeof someNamespace / typeof import(...)) have no dedicated + // relation anywhere below, so two structurally-identical namespaces — even the exact same + // one referenced twice, e.g. `true ? af : null` vs `true ? null : af` — fell through to the + // generic `return false` at the bottom, making a namespace type spuriously incompatible with + // ITSELF. Relate them structurally, like an object/record: every member `expected` exposes + // must exist on `actual` with a compatible type. + if (expected is TypeInfo.Namespace expNs && actual is TypeInfo.Namespace actNs) + { + return expNs.Types.All(kv => actNs.Types.TryGetValue(kv.Key, out var t) && IsCompatible(kv.Value, t)) + && expNs.Values.All(kv => actNs.Values.TryGetValue(kv.Key, out var v) && IsCompatible(kv.Value, v)); + } + if (expected is TypeInfo.Module expMod && actual is TypeInfo.Module actMod) + { + return expMod.Exports.All(kv => actMod.Exports.TryGetValue(kv.Key, out var v) && IsCompatible(kv.Value, v)) + && (expMod.DefaultExport is null || (actMod.DefaultExport is not null && IsCompatible(expMod.DefaultExport, actMod.DefaultExport))); + } + // Expand recursive type aliases lazily if (expected is TypeInfo.RecursiveTypeAlias expectedRTA) { From aadabac463d5a524bef8bea8e84b9ae44340747b Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Sun, 5 Jul 2026 22:54:19 -0700 Subject: [PATCH 05/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=203=20more=20tests=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 223 Pass / 55 Fail (was 219/59), 0 regressions: nullIsSubtypeOfEverythingButUndefined, subtypesOfTypeParameter, and subtypesOfTypeParameterWithConstraints2/3/4 now pass. --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index 8eae2cc1..fb88d3f9 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -247,11 +247,11 @@ tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/enumIsNotA tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/nullIsSubtypeOfEverythingButUndefined.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/stringLiteralTypeIsSubtypeOfString.ts Fail tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfAny.ts Pass -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameter.ts Fail +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameter.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints.ts Pass -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints2.ts Fail -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints3.ts Fail -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints4.ts Fail +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints2.ts Pass +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints3.ts Pass +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithConstraints4.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfTypeParameterWithRecursiveConstraints.ts Fail tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypesOfUnion.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithCallSignatures.ts Pass From 857af4ca5cb12c0578ea0e45a84bc6d2d7da3f0a Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 00:22:42 -0700 Subject: [PATCH 06/32] fix(types): add TS2464 computed-property-name validation, type Symbol/SymbolConstructor precisely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the missing TS2464 check ("a computed property name must be of type 'string', 'number', 'symbol', or 'any'") for object-literal properties, methods, and accessors with a computed key. Records rather than throws so every offending key in one literal is reported, not just the first (mirrors the existing TS2411 interface string-index-collision loop). This check is only meaningful once `Symbol` itself types precisely: - Bare `Symbol` now resolves to a real `SymbolConstructor`-shaped interface (WellKnownSymbolTypes.SymbolConstructor) instead of `any`, so e.g. `var s = Symbol; {[s]: 0}` is correctly rejected — tsc types `s` as SymbolConstructor, not `any`. - `Symbol.for`/`Symbol.keyFor`/`Symbol.prototype` referenced as plain values (not called) now get real types in CheckGet, mirroring the typed CALL form TryCheckBuiltinCall already gives Symbol.for/keyFor. Previously they fell through to evaluating bare `Symbol`=any then a member access on `any`=any. - A user-declared `interface SymbolConstructor { ... }` (tsc's declaration- merging pattern for augmenting global ambients) takes priority over the built-in shape, so tests that add their own SymbolConstructor members keep working. Not a full merge (a file mixing a custom member with a built-in one like Symbol.for would only see the custom interface) but covers the real declaration-merging tests, which never mix the two. Also fixes a pre-existing, unrelated line-attribution bug surfaced only by the above: "Property 'X' does not exist on interface" (TS2339, 3 call sites in TypeChecker.Properties.cs/Properties.Helpers.cs) never passed a `line:`, falling back to the enclosing statement's start line instead of the member token's own line — wrong whenever the access is not on the statement's first line (e.g. inside a multi-line object literal). --- TypeSystem/TypeChecker.Expressions.cs | 50 +++++++++++++++++++- TypeSystem/TypeChecker.Properties.Helpers.cs | 2 +- TypeSystem/TypeChecker.Properties.cs | 19 +++++++- TypeSystem/WellKnownSymbolTypes.cs | 37 +++++++++++++++ 4 files changed, 103 insertions(+), 5 deletions(-) diff --git a/TypeSystem/TypeChecker.Expressions.cs b/TypeSystem/TypeChecker.Expressions.cs index 572d5e7e..2fc3b94c 100644 --- a/TypeSystem/TypeChecker.Expressions.cs +++ b/TypeSystem/TypeChecker.Expressions.cs @@ -691,6 +691,7 @@ private TypeInfo CheckObject(Expr.ObjectLiteral obj) accessorProps.Add(prop); // Getter - extract return type from annotation only (don't check body yet) + ValidateComputedPropertyNameType(prop.Key!); string name = GetAccessorMemberName(prop.Key!); getterNames.Add(name); if (prop.Value is Expr.ArrowFunction arrow && arrow.ReturnType != null) @@ -709,6 +710,7 @@ private TypeInfo CheckObject(Expr.ObjectLiteral obj) accessorProps.Add(prop); // Setter - extract parameter type from annotation only + ValidateComputedPropertyNameType(prop.Key!); string name = GetAccessorMemberName(prop.Key!); setterNames.Add(name); if (prop.Value is Expr.ArrowFunction arrow && arrow.Parameters.Count > 0 && arrow.Parameters[0].Type != null) @@ -780,7 +782,10 @@ private TypeInfo CheckObject(Expr.ObjectLiteral obj) // Union of string/number types - use string index signature stringIndexType = UnifyIndexTypes(stringIndexType, valueType); else - throw new TypeCheckException($" Computed property key must be string, number, or symbol, got '{keyType}'.", tsCode: "TS1170"); + // Record (don't throw) so every offending computed key in this object + // literal is reported, not just the first — mirrors the interface + // string-index-collision loop (TS2411) elsewhere in this file. + RecordTypeError(new TypeCheckException(" A computed property name must be of type 'string', 'number', 'symbol', or 'any'.", line: TryGetExprLine(ck.Expression), tsCode: "TS2464")); break; } } @@ -883,6 +888,36 @@ private static string GetAccessorMemberName(Expr.PropertyKey key) => ? wellKnownName : GetPropertyKeyNameForTypeCheck(key); + /// + /// TS2464: an object-literal accessor's computed key (get [expr]() {}/set [expr](v) {}) + /// must be of type string/number/symbol/any, mirroring tsc. The plain-property computed-key switch + /// in validates this inline (it also needs the key's type to route index + /// signatures); accessors only extract a NAME via and never + /// evaluated/validated the key's type at all, so a non-conforming accessor key silently passed. + /// Records (doesn't throw) so multiple offending members in one literal are all reported. + /// + private void ValidateComputedPropertyNameType(Expr.PropertyKey key) + { + if (key is not Expr.ComputedKey ck) return; + TypeInfo keyType = CheckExpr(ck.Expression); + if (IsValidComputedPropertyNameType(keyType)) return; + RecordTypeError(new TypeCheckException( + " A computed property name must be of type 'string', 'number', 'symbol', or 'any'.", + line: TryGetExprLine(ck.Expression), tsCode: "TS2464")); + } + + private static bool IsValidComputedPropertyNameType(TypeInfo keyType) => keyType switch + { + TypeInfo.Any => true, + TypeInfo.String => true, + TypeInfo.StringLiteral => true, + TypeInfo.Primitive { Type: TokenType.TYPE_NUMBER } => true, + TypeInfo.NumberLiteral => true, + TypeInfo.Symbol or TypeInfo.UniqueSymbol => true, + TypeInfo.Union u => u.FlattenedTypes.All(IsValidComputedPropertyNameType), + _ => false, + }; + /// /// Unifies index signature types - creates a union if types differ. /// @@ -1744,7 +1779,18 @@ private TypeInfo LookupVariable(Token name) if (name.Lexeme == "Number") return new TypeInfo.Any(); // Number is a special global object if (name.Lexeme == "String") return new TypeInfo.Any(); // String is a special global object if (name.Lexeme == "Boolean") return new TypeInfo.Any(); // Boolean is a special global object - if (name.Lexeme == "Symbol") return new TypeInfo.Any(); // Symbol is a special global object + if (name.Lexeme == "Symbol") + { + // tsc lets user code augment the global `SymbolConstructor` interface via declaration + // merging (`interface SymbolConstructor { foo: string }` adds a real `Symbol.foo`). + // Prefer a user-declared SymbolConstructor when present so those members resolve; + // otherwise fall back to the built-in shape. Not a full merge (a file combining both a + // custom member AND a built-in one like Symbol.for would only see the custom interface), + // but covers the real declaration-merging tests, which never mix the two. + if (_environment.Get("SymbolConstructor") is TypeInfo.Interface userSymbolCtor) + return userSymbolCtor; + return WellKnownSymbolTypes.SymbolConstructor; // tsc's SymbolConstructor shape + } if (name.Lexeme == "Function") return new TypeInfo.Any(); // Function is the Function constructor if (name.Lexeme == "Proxy") return new TypeInfo.Any(); // Proxy is a special global object if (name.Lexeme == "Buffer") return new TypeInfo.Any(); // Buffer is a global constructor for binary data diff --git a/TypeSystem/TypeChecker.Properties.Helpers.cs b/TypeSystem/TypeChecker.Properties.Helpers.cs index 9e916f65..d7f1cae2 100644 --- a/TypeSystem/TypeChecker.Properties.Helpers.cs +++ b/TypeSystem/TypeChecker.Properties.Helpers.cs @@ -233,7 +233,7 @@ private TypeInfo CheckGetOnInterface(TypeInfo.Interface itf, Token memberName) { return protoMember; } - throw new TypeCheckException($" Property '{memberName.Lexeme}' does not exist on interface '{itf.Name}'.", tsCode: "TS2339"); + throw new TypeCheckException($" Property '{memberName.Lexeme}' does not exist on interface '{itf.Name}'.", line: memberName.Line, tsCode: "TS2339"); } /// diff --git a/TypeSystem/TypeChecker.Properties.cs b/TypeSystem/TypeChecker.Properties.cs index 45d0a6b7..8f14199c 100644 --- a/TypeSystem/TypeChecker.Properties.cs +++ b/TypeSystem/TypeChecker.Properties.cs @@ -107,6 +107,21 @@ private TypeInfo CheckGet(Expr.Get get) var wellKnownType = WellKnownSymbolTypes.TryGet(get.Name.Lexeme); if (wellKnownType != null) return wellKnownType; + + // Symbol.for/keyFor/prototype referenced as plain values (not called) — mirrors the + // typed signatures TryCheckBuiltinCall already gives the CALL form of Symbol.for/keyFor. + // Without this, bare `Symbol.for` fell through to CheckExpr(Symbol)=Any then `.for` on + // Any=Any, silently accepting it wherever a real, non-`any` type is required (e.g. a + // computed property name, TS2464) even though tsc types it as a real function/object. + switch (get.Name.Lexeme) + { + case "for": + return new TypeInfo.Function([new TypeInfo.String()], new TypeInfo.Symbol(), 1, false, null, ["key"]); + case "keyFor": + return new TypeInfo.Function([new TypeInfo.Symbol()], new TypeInfo.Union([new TypeInfo.String(), new TypeInfo.Undefined()]), 1, false, null, ["sym"]); + case "prototype": + return new TypeInfo.Record(FrozenDictionary.Empty); + } } // Check for property narrowing (e.g., after "if (obj.prop !== null)" or nested "obj.a.b") @@ -700,7 +715,7 @@ private TypeInfo CheckSet(Expr.Set set) return valueType; } } - throw new TypeCheckException($" Property '{set.Name.Lexeme}' does not exist on interface '{itf.Name}'.", tsCode: "TS2339"); + throw new TypeCheckException($" Property '{set.Name.Lexeme}' does not exist on interface '{itf.Name}'.", line: set.Name.Line, tsCode: "TS2339"); } // Handle Error property assignment (name, message, stack are mutable strings; cause is any) if (objType is TypeInfo.Error) @@ -818,7 +833,7 @@ private TypeInfo CheckGetOnType(TypeInfo objType, Token memberName) return member.Value; } } - throw new TypeCheckException($" Property '{memberName.Lexeme}' does not exist on interface '{itf.Name}'.", tsCode: "TS2339"); + throw new TypeCheckException($" Property '{memberName.Lexeme}' does not exist on interface '{itf.Name}'.", line: memberName.Line, tsCode: "TS2339"); } // Handle Record type - check fields and index signatures diff --git a/TypeSystem/WellKnownSymbolTypes.cs b/TypeSystem/WellKnownSymbolTypes.cs index f4ec38f4..e667c401 100644 --- a/TypeSystem/WellKnownSymbolTypes.cs +++ b/TypeSystem/WellKnownSymbolTypes.cs @@ -1,3 +1,5 @@ +using System.Collections.Frozen; + namespace SharpTS.TypeSystem; /// @@ -61,4 +63,39 @@ public static class WellKnownSymbolTypes "split" => Split, _ => null }; + + /// + /// The type of the bare `Symbol` value itself (tsc's `SymbolConstructor`). Used wherever + /// `Symbol` is referenced WITHOUT being called — e.g. `var s = Symbol; obj[s]`. Deliberately + /// NOT `any`: a bare-Symbol-derived value must fail non-permissive checks like a computed + /// property name's "must be string/number/symbol/any" validation (TS2464), the way tsc's real + /// SymbolConstructor object type does. `Symbol(...)` calls and `Symbol.iterator`-style + /// well-known-symbol access are matched by earlier special cases (TryCheckBuiltinCall / + /// CheckGet) before generic member lookup ever consults this type, so adding it here doesn't + /// change those paths — only genuine "use Symbol as a plain value" sites. + /// + public static readonly TypeInfo.Interface SymbolConstructor = new( + "SymbolConstructor", + new Dictionary + { + ["iterator"] = Iterator, + ["asyncIterator"] = AsyncIterator, + ["toStringTag"] = ToStringTag, + ["hasInstance"] = HasInstance, + ["isConcatSpreadable"] = IsConcatSpreadable, + ["toPrimitive"] = ToPrimitive, + ["species"] = Species, + ["unscopables"] = Unscopables, + ["dispose"] = Dispose, + ["asyncDispose"] = AsyncDispose, + ["match"] = Match, + ["matchAll"] = MatchAll, + ["replace"] = Replace, + ["search"] = Search, + ["split"] = Split, + ["for"] = new TypeInfo.Function([new TypeInfo.String()], new TypeInfo.Symbol(), 1, false, null, ["key"]), + ["keyFor"] = new TypeInfo.Function([new TypeInfo.Symbol()], new TypeInfo.Union([new TypeInfo.String(), new TypeInfo.Undefined()]), 1, false, null, ["sym"]), + ["prototype"] = new TypeInfo.Record(FrozenDictionary.Empty), + }.ToFrozenDictionary(), + FrozenSet.Empty); } From adfe251602f3f26cba52415f44ab07c41dfc2c16 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 00:22:42 -0700 Subject: [PATCH 07/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolProperty3/52/54=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 226 Pass / 53 Fail (was 223/55), 0 regressions. --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index fb88d3f9..1fa9bb3a 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -104,7 +104,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty26.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty27.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty28.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty29.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty3.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty3.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty30.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty31.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty32.ts Fail @@ -129,9 +129,9 @@ tests/cases/conformance/es6/Symbols/symbolProperty49.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty5.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty50.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty51.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty52.ts Skipped:lib-drift +tests/cases/conformance/es6/Symbols/symbolProperty52.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty53.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty54.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty54.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty55.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty56.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty57.ts Pass From 0130fb40f46e1278f31e6daa250210a6c88f6a10 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 00:53:15 -0700 Subject: [PATCH 08/32] fix(types): detect duplicate well-known-symbol object-literal keys and class accessors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent missing duplicate-identifier checks, both surfaced by a well-known-symbol computed key colliding with itself: - TS1117 ("An object literal cannot have multiple properties with the same name"): a second plain property with the same canonical well-known-symbol name (`{[Symbol.isConcatSpreadable]: 0, [Symbol.isConcatSpreadable]: 1}`) silently overwrote the first in CheckObject's fields dict. Getter/setter pairs for the same name are exempt (legitimate); a second plain duplicate is not. - TS2300 ("Duplicate identifier"): class accessors were never checked for duplicates at all — two getters (or two setters) with the same name/ static-ness silently overwrote each other in mutableClass.Getters/Setters. Fixed for both plain AND well-known-symbol computed accessor names (the computed case had no name-tracking of any kind before — CheckClassMembers skipped it entirely, "no static member name to register"). --- TypeSystem/TypeChecker.Expressions.cs | 11 ++++++++++ TypeSystem/TypeChecker.Statements.Classes.cs | 23 ++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/TypeSystem/TypeChecker.Expressions.cs b/TypeSystem/TypeChecker.Expressions.cs index 2fc3b94c..30d0a8eb 100644 --- a/TypeSystem/TypeChecker.Expressions.cs +++ b/TypeSystem/TypeChecker.Expressions.cs @@ -762,10 +762,21 @@ private TypeInfo CheckObject(Expr.ObjectLiteral obj) // Infer index signature based on key type if (keyType is (TypeInfo.Symbol or TypeInfo.UniqueSymbol) && TryGetWellKnownSymbolMemberName(ck.Expression) is { } wellKnownSymbolName) + { // Symbol.iterator/toStringTag/toPrimitive/... — a named member (canonical // "@@name", matching interfaces/classes), not merged into the symbol index // signature. Merging would lose the per-symbol type (#99 Cluster B). + // TS1117: tsc rejects a second plain (non-accessor) property with the same + // canonical name — an object literal, unlike a class/interface, can't + // overload. Getter/setter pairs are exempt (handled in their own branches, + // which never touch `fields` at this point) but a second PLAIN duplicate is not. + if (!getterNames.Contains(wellKnownSymbolName) && !setterNames.Contains(wellKnownSymbolName) + && fields.ContainsKey(wellKnownSymbolName)) + RecordTypeError(new TypeCheckException( + " An object literal cannot have multiple properties with the same name.", + line: TryGetExprLine(ck.Expression), tsCode: "TS1117")); fields[wellKnownSymbolName] = valueType; + } else if (keyType is TypeInfo.String) stringIndexType = UnifyIndexTypes(stringIndexType, valueType); else if (keyType is TypeInfo.Primitive n && n.Type == TokenType.TYPE_NUMBER) diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index a8b58cd9..c033f3ae 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -285,6 +285,13 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // Collect accessor types if (classStmt.Accessors != null) { + // TS2300: two getters (or two setters) with the same name/static-ness are a duplicate + // identifier — a class, unlike an object literal, can't have two same-kind accessors for + // one member at all (a get+set PAIR is fine; that's tracked separately below). Covers both + // plain and well-known-symbol computed names (`get [Symbol.hasInstance]()` declared twice), + // neither of which had any duplicate-accessor detection before. + var seenGetters = new Dictionary<(bool IsStatic, string Name), int>(); + var seenSetters = new Dictionary<(bool IsStatic, string Name), int>(); foreach (var accessor in classStmt.Accessors) { // Check accessor decorators @@ -293,6 +300,22 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) : DecoratorTarget.Setter; CheckDecorators(accessor.Decorators, accessorTarget); + string? canonicalName = accessor.ComputedKey != null + ? TryGetWellKnownSymbolMemberName(accessor.ComputedKey) + : accessor.Name.Lexeme; + if (canonicalName != null) + { + var seen = accessor.Kind.Type == TokenType.GET ? seenGetters : seenSetters; + var key = (accessor.IsStatic, canonicalName); + int line = TryGetExprLine(accessor.ComputedKey) ?? accessor.Name.Line; + if (!seen.TryAdd(key, line)) + { + string displayName = accessor.ComputedKey != null ? $"[Symbol.{canonicalName["@@".Length..]}]" : canonicalName; + RecordTypeError(new TypeCheckException( + $" Duplicate identifier '{displayName}'.", line: line, tsCode: "TS2300")); + } + } + // Computed accessor names (get [Symbol.x]()) have no static member // name to register; their bodies are still checked later. if (accessor.ComputedKey != null) From f0bee95d4e5990bb069c4d565d030c5b1f6941fc Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 00:53:16 -0700 Subject: [PATCH 09/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolProperty36/44=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 228 Pass / 51 Fail (was 226/53), 0 regressions. --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index 1fa9bb3a..bf3771f4 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -111,7 +111,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty32.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty33.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty34.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty35.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty36.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty36.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty37.ts TypeCheckError tests/cases/conformance/es6/Symbols/symbolProperty38.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty39.ts Fail @@ -120,7 +120,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty40.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty41.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty42.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty43.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty44.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty44.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty45.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty46.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty47.ts Fail From 4aa5bf5a4efa677a405c1586a9faf30176a3181d Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 02:36:27 -0700 Subject: [PATCH 10/32] fix(types): validate overloads for computed well-known-symbol class methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Computed symbol-keyed methods (`[Symbol.iterator]() {...}`) were entirely excluded from the class's method-overload grouping (`.Where(m => m.ComputedKey == null)`), and separately handled by a loop that only considered bodies (`m.Body != null`) — so overload signatures with no body were silently dropped, and multiple implementations just overwrote each other in file order with no duplicate detection at all. Replaced that loop with proper grouping by canonical well-known-symbol name (across BOTH static and instance declarations — needed so the new static- consistency check below can compare across the static/instance boundary, not just within it), running the same validation string-named methods already got: - TS2393 ("Duplicate function implementation"): now fires on every declaration in the group (signatures included) once a second implementation appears, matching tsc exactly rather than silently keeping the last one. - TS2391 ("Function implementation is missing..."): fires on the last declaration when a group has signatures but no implementation. - TS2387/TS2388 ("Function overload must (not) be static"): brand new check, previously missing for every method (not just computed-key ones). tsc compares consecutive declarations pairwise and reports the mismatch on the later one, with the message chosen by the earlier one's own static-ness — verified against symbolProperty42's exact (line, code) pairs empirically, since deriving it from the compiler source's neighbor-pair algorithm didn't cleanly map to a single forward pass. The existing generator-return special-casing for [Symbol.iterator]/ [Symbol.asyncIterator] factory signatures is preserved, now applied to whichever declaration is chosen as the group's representative (the sole implementation, or the last declaration as a best-effort fallback when the group is itself erroneous). --- TypeSystem/TypeChecker.Statements.Classes.cs | 79 ++++++++++++++++---- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index c033f3ae..d501047b 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -120,27 +120,78 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // Computed symbol-keyed methods (`[Symbol.iterator]() {...}`) are modeled under their canonical // well-known-symbol member name (@@iterator, @@asyncIterator, …) so structural iterability and // member lookup see them (#592/#485). They carry the synthetic `` name, so they are - // pulled out of the name-keyed overload grouping below. Arbitrary computed keys (e.g. `[v]()`) - // aren't statically named members and are skipped (still parsed/checked, just untyped — like - // computed accessors). - foreach (var method in classStmt.Methods.Where(m => m.ComputedKey != null && m.Body != null)) + // pulled out of the name-keyed overload grouping below and grouped here instead by (IsStatic, + // canonical name) — running the SAME overload/duplicate-implementation/static-consistency + // validation string-named methods get, rather than silently "last one wins" registration. + // Arbitrary computed keys (e.g. `[v]()`) aren't statically named members and are skipped + // (still parsed/checked, just untyped — like computed accessors). + var computedMethodGroups = classStmt.Methods + .Where(m => m.ComputedKey != null) + .Select(m => (Method: m, Name: TryGetWellKnownSymbolMemberName(m.ComputedKey))) + .Where(x => x.Name != null) + // Grouped by canonical NAME ONLY (not IsStatic too): the static-consistency check + // below needs static AND instance declarations of the same name in one group to + // compare them against each other — grouping by (IsStatic, Name) would silently + // split a static/instance mismatch into two single-member groups that never meet. + .GroupBy(x => x.Name!) + .ToList(); + + foreach (var group in computedMethodGroups) { - if (TryGetWellKnownSymbolMemberName(method.ComputedKey) is not { } memberName) - continue; - // Build the factory signature WITHOUT the generator-return wrapping BuildMethodFuncType - // applies: a [Symbol.iterator]()/[Symbol.asyncIterator]() factory must return the iterator - // type itself (e.g. `Iterator` — what for...of consumes and reads its element from), - // not Generator>. An un-annotated generator factory stays here + string memberName = group.Key; + var members = group.Select(x => x.Method).ToList(); + + // TS2387/TS2388: tsc requires every declaration in an overload group to agree on + // static-ness. It compares consecutive pairs in source order and reports the mismatch + // at the LATER member, choosing the message from the EARLIER member's own static-ness + // ("must be static" when the earlier one was static, "must not be static" otherwise) — + // verified against symbolProperty42's exact (line, code) pairs. + for (int i = 1; i < members.Count; i++) + { + if (members[i - 1].IsStatic != members[i].IsStatic) + { + var (msg, code) = members[i - 1].IsStatic + ? (" Function overload must be static.", "TS2387") + : (" Function overload must not be static.", "TS2388"); + RecordTypeError(new TypeCheckException(msg, line: members[i].Name.Line, tsCode: code)); + } + } + + var signatures = members.Where(m => m.Body == null && !m.IsAbstract).ToList(); + var implementations = members.Where(m => m.Body != null).ToList(); + + if (implementations.Count > 1) + { + // tsc flags EVERY declaration in the group once a second implementation appears — + // signatures included, not just the implementations. + foreach (var m in members) + RecordTypeError(new TypeCheckException(" Duplicate function implementation.", line: m.Name.Line, tsCode: "TS2393")); + } + else if (implementations.Count == 0 && signatures.Count > 0) + { + RecordTypeError(new TypeCheckException( + " Function implementation is missing or not immediately following the declaration.", + line: members[^1].Name.Line, tsCode: "TS2391")); + } + + // Register the group's type from the single implementation when there is exactly one + // (the common, error-free case), else the last declaration as a best-effort fallback so + // an error'd group still gets SOME type. Build the factory signature WITHOUT the + // generator-return wrapping BuildMethodFuncType applies: a [Symbol.iterator]()/ + // [Symbol.asyncIterator]() factory must return the iterator type itself (e.g. + // `Iterator` — what for...of consumes and reads its element from), not + // Generator>. An un-annotated generator factory stays here // and is filled in by the inferred-return post-pass below as Generator. + var representative = implementations.Count == 1 ? implementations[0] : members[^1]; var (cParamTypes, cRequired, cHasRest, cParamNames) = BuildFunctionSignature( - method.Parameters, validateDefaults: true, contextName: $"method '{memberName}'"); - TypeInfo factoryReturn = ResolveAnnotation(method.ReturnType, method.ReturnTypeNode) ?? new TypeInfo.Inferred(); + representative.Parameters, validateDefaults: true, contextName: $"method '{memberName}'"); + TypeInfo factoryReturn = ResolveAnnotation(representative.ReturnType, representative.ReturnTypeNode) ?? new TypeInfo.Inferred(); var funcType = new TypeInfo.Function(cParamTypes, factoryReturn, cRequired, cHasRest, null, cParamNames); - if (method.IsStatic) + if (representative.IsStatic) mutableClass.StaticMethods[memberName] = funcType; else mutableClass.Methods[memberName] = funcType; - (method.IsStatic ? mutableClass.StaticMethodAccess : mutableClass.MethodAccess)[memberName] = method.Access; + (representative.IsStatic ? mutableClass.StaticMethodAccess : mutableClass.MethodAccess)[memberName] = representative.Access; } // First pass: collect signatures, grouping overloads From 495a4ce29f77dcf3586498b490d55c6d033b0ed3 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 02:36:27 -0700 Subject: [PATCH 11/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolProperty39/42/43=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 231 Pass / 48 Fail (was 228/51), 0 regressions. --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index bf3771f4..c60019b5 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -114,12 +114,12 @@ tests/cases/conformance/es6/Symbols/symbolProperty35.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty36.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty37.ts TypeCheckError tests/cases/conformance/es6/Symbols/symbolProperty38.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty39.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty39.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty4.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty40.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty41.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty42.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty43.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty42.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty43.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty44.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty45.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty46.ts Fail From f255347dcde1034b1dba871ef06d66678be8ca85 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 02:50:43 -0700 Subject: [PATCH 12/32] fix(types): flag a class accessor that collides with a same-named method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A class member can't be both a method and an accessor, but nothing checked for it: `[Symbol.toPrimitive](x: I) {}` followed by `get [Symbol.toPrimitive]()`/ `set [Symbol.toPrimitive](x)` silently registered all three under the same canonical name with no duplicate-identifier error. Methods are registered before accessors are processed, so each accessor now also checks whether a method already claimed its (IsStatic, name) key — tsc flags the accessor, not the method that established the name first. --- TypeSystem/TypeChecker.Statements.Classes.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index d501047b..5ca561e2 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -356,12 +356,16 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) : accessor.Name.Lexeme; if (canonicalName != null) { + int line = TryGetExprLine(accessor.ComputedKey) ?? accessor.Name.Line; + string displayName = accessor.ComputedKey != null ? $"[Symbol.{canonicalName["@@".Length..]}]" : canonicalName; + // A method already registered under this name (methods are processed above) + // makes ANY accessor of the same name a duplicate too — a class member can't be + // both a method and an accessor. tsc flags the accessor, not the method. + var methodDict = accessor.IsStatic ? mutableClass.StaticMethods : mutableClass.Methods; var seen = accessor.Kind.Type == TokenType.GET ? seenGetters : seenSetters; var key = (accessor.IsStatic, canonicalName); - int line = TryGetExprLine(accessor.ComputedKey) ?? accessor.Name.Line; - if (!seen.TryAdd(key, line)) + if (methodDict.ContainsKey(canonicalName) || !seen.TryAdd(key, line)) { - string displayName = accessor.ComputedKey != null ? $"[Symbol.{canonicalName["@@".Length..]}]" : canonicalName; RecordTypeError(new TypeCheckException( $" Duplicate identifier '{displayName}'.", line: line, tsCode: "TS2300")); } From 1e7cc8b995362211073a4e433988ff21babc5805 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 02:50:43 -0700 Subject: [PATCH 13/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolDeclarationEmit12=20now=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 232 Pass / 47 Fail (was 231/48), 0 regressions. --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index c60019b5..ab73ec76 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -71,7 +71,7 @@ tests/cases/conformance/es2023/intlNumberFormatES5UseGrouping.ts Fail tests/cases/conformance/es6/Symbols/symbolDeclarationEmit1.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit10.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit11.ts Pass -tests/cases/conformance/es6/Symbols/symbolDeclarationEmit12.ts Fail +tests/cases/conformance/es6/Symbols/symbolDeclarationEmit12.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit13.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit14.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit2.ts Pass From c15c43d26258f13cd00b2894f0d18f15ad0ca79f Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 10:44:18 -0700 Subject: [PATCH 14/32] fix(types): fix swapped-branch bug in nullish-coalescing common-type selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CheckNullishCoalescing had the same "pick wider of two types, return the wrong one" anti-pattern already found in CheckLogicalAssign and CheckTernary this epic: it picked the wider of the non-nullish-left/right types via IsCompatible(A,B) || IsCompatible(B,A), but always returned the left operand regardless of which clause fired. When the right operand was actually the wider type, the narrower left leaked out instead. Audited CheckLogical (&&/||) as the other unaudited sibling from prior rounds — it was already correct (two separate returns, each matching its own branch). The other two `IsCompatible(A,B) || IsCompatible(B,A)` hits in the codebase (generic bivariant-variance boolean check, type-assertion escape hatch) are unrelated patterns, not instances of this bug. --- TypeSystem/TypeChecker.Operators.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/TypeSystem/TypeChecker.Operators.cs b/TypeSystem/TypeChecker.Operators.cs index ffa49ee3..7dfca285 100644 --- a/TypeSystem/TypeChecker.Operators.cs +++ b/TypeSystem/TypeChecker.Operators.cs @@ -285,11 +285,17 @@ private TypeInfo CheckNullishCoalescing(Expr.NullishCoalescing nc) return rightType; // null/undefined ?? right = right } - // If left (non-nullish) and right are compatible, return non-nullish left - if (IsCompatible(nonNullishLeft, rightType) || IsCompatible(rightType, nonNullishLeft)) + // Return whichever of the two is the wider type (the other is assignable to it) — same + // "pick wider of two" pattern as CheckTernary/CheckLogicalAssign; this one previously + // returned nonNullishLeft unconditionally even when rightType was the wider of the two. + if (IsCompatible(nonNullishLeft, rightType)) { return nonNullishLeft; } + if (IsCompatible(rightType, nonNullishLeft)) + { + return rightType; + } // Otherwise return union of non-nullish left and right return new TypeInfo.Union([nonNullishLeft, rightType]); From 32ff03233282bee01b826238e2286eb08421a22b Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 10:44:19 -0700 Subject: [PATCH 15/32] fix(types): validate well-known-symbol interface members against a symbol index signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interfaces already checked explicit properties against a string index signature (TS2411) but had no equivalent for a symbol index signature ([s: symbol]: T) — a computed well-known-symbol member (canonical "@@name") could freely violate it. Added the mirror-image check, exempting plain string/number-keyed members the same way the string-index check exempts symbol-keyed ones. Class-side symbol-index checks (own AND cross-hierarchy — symbolProperty30/ 32/34) are NOT covered here: they need per-method (not just per-field) index validation plus, for 34 specifically, an unrelated new "class used before its declaration" check (TS2449). Scoped out as bigger, separate work. --- .../TypeChecker.Statements.Interfaces.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/TypeSystem/TypeChecker.Statements.Interfaces.cs b/TypeSystem/TypeChecker.Statements.Interfaces.cs index 3799913a..bcc1eedc 100644 --- a/TypeSystem/TypeChecker.Statements.Interfaces.cs +++ b/TypeSystem/TypeChecker.Statements.Interfaces.cs @@ -343,6 +343,26 @@ private void CheckInterfaceDeclaration(Stmt.Interface interfaceStmt) } } } + + // Mirror image of the string-index check above: a symbol index signature only + // constrains computed well-known-symbol members (canonical "@@name"), never + // plain string/number-keyed ones. + if (symbolIndexType != null) + { + foreach (var (name, type) in members) + { + if (!name.StartsWith("@@", StringComparison.Ordinal)) continue; + if (!IsCompatible(symbolIndexType, type)) + { + memberLines.TryGetValue(name, out var line); + string displayName = $"[Symbol.{name["@@".Length..]}]"; + RecordTypeError(new TypeCheckException( + $" Property '{displayName}' of type '{type}' is not assignable to 'symbol' index type '{symbolIndexType}'.", + line: line == 0 ? interfaceStmt.Name.Line : line, + tsCode: "TS2411")); + } + } + } } } From 1e013dc7e3ed01af15b975fd96a61694ce9c7f3b Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 6 Jul 2026 10:44:19 -0700 Subject: [PATCH 16/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolProperty17=20now=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 233 Pass / 46 Fail (was 232/47), 0 regressions. --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index ab73ec76..9c0f2d0b 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -90,7 +90,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty13.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty14.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty15.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty16.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty17.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty17.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty18.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty19.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty2.ts Pass From df055f6c3770697c86eba1fd5c4e6883ef0cca64 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 19:16:02 -0700 Subject: [PATCH 17/32] fix(types): validate symbol operands in in/for-in/delete/new and interface names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the checks tsc emits for symbol used where it isn't allowed: - 'in' right operand must be an object (TS2322) - for-in right operand may not be symbol (TS2407); an existing symbol-typed loop variable fails the left-hand side (TS2405) — needs a new IsDeclaration flag on Stmt.ForIn to tell 'for (x in o)' from 'for (var x in o)' - delete of a read-only property is TS2704; the well-known-symbol members of SymbolConstructor are now marked readonly so 'delete Symbol.iterator' fires - new Symbol() is TS2350 (Symbol has no construct signature) - an interface may not be named 'symbol' (TS2427) Flips symbolType2/3/13/14/20. instanceof operand validation (symbolType1) is deferred: it needs 'Symbol() || {}' to stay a symbol|{} union, but CheckLogical collapses it to a bare symbol. --- Parsing/AST.cs | 4 ++- Parsing/Parser.Statements.cs | 2 +- TypeSystem/TypeChecker.Operators.cs | 28 ++++++++++++++++++- TypeSystem/TypeChecker.Properties.New.cs | 7 +++++ .../TypeChecker.Statements.Interfaces.cs | 7 +++++ TypeSystem/TypeChecker.Statements.cs | 18 ++++++++++++ TypeSystem/WellKnownSymbolTypes.cs | 10 ++++++- 7 files changed, 72 insertions(+), 4 deletions(-) diff --git a/Parsing/AST.cs b/Parsing/AST.cs index a5dac16c..6a115ded 100644 --- a/Parsing/AST.cs +++ b/Parsing/AST.cs @@ -440,7 +440,9 @@ public record While(Expr Condition, Stmt Body) : Stmt; public record For(Stmt? Initializer, Expr? Condition, Expr? Increment, Stmt Body) : Stmt; public record DoWhile(Stmt Body, Expr Condition) : Stmt; public record ForOf(Token Variable, string? TypeAnnotation, Expr Iterable, Stmt Body, bool IsAsync = false) : Stmt; - public record ForIn(Token Variable, string? TypeAnnotation, Expr Object, Stmt Body) : Stmt; + // IsDeclaration distinguishes `for (var x in o)` (a fresh binding) from `for (x in o)` + // (assignment to an existing lvalue) — the latter's target type is validated (TS2405). + public record ForIn(Token Variable, string? TypeAnnotation, Expr Object, Stmt Body, bool IsDeclaration = true) : Stmt; public record If(Expr Condition, Stmt ThenBranch, Stmt? ElseBranch) : Stmt; public record Print(Expr Expr) : Stmt; // Temporary for console.log public record Break(Token Keyword, Token? Label = null) : Stmt; diff --git a/Parsing/Parser.Statements.cs b/Parsing/Parser.Statements.cs index 13d8f939..0f2c7a74 100644 --- a/Parsing/Parser.Statements.cs +++ b/Parsing/Parser.Statements.cs @@ -168,7 +168,7 @@ private Stmt ForStatement() Stmt body = Statement(); if (isOfLoop) return new Stmt.ForOf(varName, null, rhs, body, isAsync); - return new Stmt.ForIn(varName, null, rhs, body); + return new Stmt.ForIn(varName, null, rhs, body, IsDeclaration: false); } // Traditional for loop without let/const diff --git a/TypeSystem/TypeChecker.Operators.cs b/TypeSystem/TypeChecker.Operators.cs index 7dfca285..a853fca1 100644 --- a/TypeSystem/TypeChecker.Operators.cs +++ b/TypeSystem/TypeChecker.Operators.cs @@ -29,11 +29,28 @@ private TypeInfo CheckBinary(Expr.Binary binary) OperatorDescriptor.Equality => new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN), OperatorDescriptor.Bitwise or OperatorDescriptor.BitwiseShift => CheckBitwiseBinary(left, right, line), OperatorDescriptor.UnsignedRightShift => CheckUnsignedShiftBinary(left, right, line), - OperatorDescriptor.In or OperatorDescriptor.InstanceOf => new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN), + OperatorDescriptor.In => CheckInOperator(right, line), + // instanceof operand validation (TS2358/TS2359) is intentionally NOT done here: the + // symbolType1 case needs `Symbol() || {}` to stay a `symbol | {}` union, but CheckLogical + // currently collapses it to a bare `symbol`, which would make a bare-symbol LHS check + // fire spuriously on `(Symbol() || {}) instanceof Object`. Deferred with that inference gap. + OperatorDescriptor.InstanceOf => new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN), _ => new TypeInfo.Any() }; } + /// + /// The `in` operator: the right operand must be an object / `any` / type parameter. tsc rejects + /// a symbol right operand (`"" in Symbol.toPrimitive`) with TS2322. Scoped to symbol so we don't + /// newly reject other non-object right operands the checker currently tolerates. + /// + private TypeInfo CheckInOperator(TypeInfo right, int line) + { + if (right is TypeInfo.Symbol or TypeInfo.UniqueSymbol) + throw new TypeCheckException($"Type '{right}' is not assignable to type 'object'.", line > 0 ? line : null, tsCode: "TS2322"); + return new TypeInfo.Primitive(TokenType.TYPE_BOOLEAN); + } + /// /// True if this type is Any, Inferred, or a Union containing an Any/Inferred arm. /// A union that includes 'any' is effectively permissive (JS semantics); operators @@ -724,6 +741,15 @@ private TypeInfo CheckNonNullAssertion(Expr.NonNullAssertion nna) private TypeInfo CheckDelete(Expr.Delete delete) { + // `delete x.p` where p is a read-only property is TS2704. Detect it on the receiver's + // type before the generic operand check (e.g. `delete Symbol.iterator` — the well-known + // symbol members of SymbolConstructor are readonly). + if (delete.Operand is Expr.Get get) + { + TypeInfo recvType = CheckExpr(get.Object); + if (recvType is TypeInfo.Interface itf && itf.IsMemberReadonly(get.Name.Lexeme)) + throw new TypeCheckException("The operand of a 'delete' operator cannot be a read-only property.", line: get.Name.Line, tsCode: "TS2704"); + } // Type check the operand (for any side effects or errors) CheckExpr(delete.Operand); // delete always returns boolean diff --git a/TypeSystem/TypeChecker.Properties.New.cs b/TypeSystem/TypeChecker.Properties.New.cs index d5cf0c7a..20545214 100644 --- a/TypeSystem/TypeChecker.Properties.New.cs +++ b/TypeSystem/TypeChecker.Properties.New.cs @@ -88,6 +88,13 @@ private TypeInfo CheckNew(Expr.New newExpr) bool isSimpleName = IsSimpleIdentifier(newExpr.Callee); string? simpleClassName = GetSimpleClassName(newExpr.Callee); + // `new Symbol()` — the Symbol function's call signature returns a (non-void) symbol and it + // has no construct signature, so calling it with `new` is TS2350. + if (isSimpleName && simpleClassName == "Symbol") + { + throw new TypeCheckException("Only a void function can be called with the 'new' keyword.", tsCode: "TS2350"); + } + // Handle new Date() constructor if (isSimpleName && simpleClassName == "Date") { diff --git a/TypeSystem/TypeChecker.Statements.Interfaces.cs b/TypeSystem/TypeChecker.Statements.Interfaces.cs index bcc1eedc..dd5c4760 100644 --- a/TypeSystem/TypeChecker.Statements.Interfaces.cs +++ b/TypeSystem/TypeChecker.Statements.Interfaces.cs @@ -186,6 +186,13 @@ private void PreRegisterInterface(Stmt.Interface interfaceStmt) private void CheckInterfaceDeclaration(Stmt.Interface interfaceStmt) { + // An interface may not be named after a primitive type keyword (TS2427). `symbol` is a + // contextual keyword the parser accepts as an identifier, so it reaches here. + if (interfaceStmt.Name.Lexeme == "symbol") + { + throw new TypeCheckException("Interface name cannot be 'symbol'.", line: interfaceStmt.Name.Line, tsCode: "TS2427"); + } + // Handle generic type parameters with two-pass approach to support recursive constraints (e.g., T extends TreeNode) List? interfaceTypeParams = null; TypeEnvironment interfaceTypeEnv = new(_environment); diff --git a/TypeSystem/TypeChecker.Statements.cs b/TypeSystem/TypeChecker.Statements.cs index 959e714c..584a9e9f 100644 --- a/TypeSystem/TypeChecker.Statements.cs +++ b/TypeSystem/TypeChecker.Statements.cs @@ -966,11 +966,29 @@ internal VoidResult VisitForIn(Stmt.ForIn stmt) { TypeInfo objType = CheckExpr(stmt.Object); + // The right-hand side must be an object type / `any` / a type parameter; a symbol fails (TS2407). + if (ContainsSymbolType(objType)) + { + throw new TypeCheckException( + $"The right-hand side of a 'for...in' statement must be of type 'any', an object type or a type parameter, but here has type '{objType}'.", + line: stmt.Variable.Line, tsCode: "TS2407"); + } + if (objType is not (TypeInfo.Record or TypeInfo.Instance or TypeInfo.Array or TypeInfo.Any or TypeInfo.Class)) { throw new TypeCheckException($"'for...in' requires an object, got {objType}", tsCode: "TS2549"); } + // When the loop variable is an existing lvalue (not a fresh `var`/`let` binding), the loop + // writes string keys into it — a symbol-typed target fails (TS2405, "must be string or any"). + if (!stmt.IsDeclaration && _environment.Get(stmt.Variable.Lexeme) is TypeInfo existingTarget + && existingTarget is TypeInfo.Symbol or TypeInfo.UniqueSymbol) + { + throw new TypeCheckException( + "The left-hand side of a 'for...in' statement must be of type 'string' or 'any'.", + line: stmt.Variable.Line, tsCode: "TS2405"); + } + TypeEnvironment forInEnv = new(_environment); forInEnv.Define(stmt.Variable.Lexeme, new TypeInfo.String()); diff --git a/TypeSystem/WellKnownSymbolTypes.cs b/TypeSystem/WellKnownSymbolTypes.cs index e667c401..ef7333fd 100644 --- a/TypeSystem/WellKnownSymbolTypes.cs +++ b/TypeSystem/WellKnownSymbolTypes.cs @@ -97,5 +97,13 @@ public static class WellKnownSymbolTypes ["keyFor"] = new TypeInfo.Function([new TypeInfo.Symbol()], new TypeInfo.Union([new TypeInfo.String(), new TypeInfo.Undefined()]), 1, false, null, ["sym"]), ["prototype"] = new TypeInfo.Record(FrozenDictionary.Empty), }.ToFrozenDictionary(), - FrozenSet.Empty); + FrozenSet.Empty, + // The well-known symbol members are `readonly` in lib.d.ts — `delete Symbol.iterator` + // is TS2704, and reassigning them is an error too. + ReadonlyMembers: new[] + { + "iterator", "asyncIterator", "toStringTag", "hasInstance", "isConcatSpreadable", + "toPrimitive", "species", "unscopables", "dispose", "asyncDispose", + "match", "matchAll", "replace", "search", "split", + }.ToFrozenSet()); } From fa46f5c492eaed7064c6a30a865e5470c4abacb5 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 19:16:02 -0700 Subject: [PATCH 18/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolType2/3/13/14/20=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../baselines/interpreted.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index 9c0f2d0b..a4597785 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -147,16 +147,16 @@ tests/cases/conformance/es6/Symbols/symbolType1.ts Fail tests/cases/conformance/es6/Symbols/symbolType10.ts Pass tests/cases/conformance/es6/Symbols/symbolType11.ts Pass tests/cases/conformance/es6/Symbols/symbolType12.ts Pass -tests/cases/conformance/es6/Symbols/symbolType13.ts Fail -tests/cases/conformance/es6/Symbols/symbolType14.ts Fail +tests/cases/conformance/es6/Symbols/symbolType13.ts Pass +tests/cases/conformance/es6/Symbols/symbolType14.ts Pass tests/cases/conformance/es6/Symbols/symbolType15.ts Fail tests/cases/conformance/es6/Symbols/symbolType16.ts Pass tests/cases/conformance/es6/Symbols/symbolType17.ts Pass tests/cases/conformance/es6/Symbols/symbolType18.ts Pass tests/cases/conformance/es6/Symbols/symbolType19.ts Pass -tests/cases/conformance/es6/Symbols/symbolType2.ts Fail -tests/cases/conformance/es6/Symbols/symbolType20.ts Fail -tests/cases/conformance/es6/Symbols/symbolType3.ts Fail +tests/cases/conformance/es6/Symbols/symbolType2.ts Pass +tests/cases/conformance/es6/Symbols/symbolType20.ts Pass +tests/cases/conformance/es6/Symbols/symbolType3.ts Pass tests/cases/conformance/es6/Symbols/symbolType4.ts Pass tests/cases/conformance/es6/Symbols/symbolType5.ts Pass tests/cases/conformance/es6/Symbols/symbolType6.ts Pass From 990cd22c5455ea44acc15ebf623d3220fe16c655 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 19:26:28 -0700 Subject: [PATCH 19/32] fix(types): flag a class used before its declaration in an extends clause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tsc reports TS2449 when a class value is referenced in an earlier (or its own) class's extends clause before that class is textually declared — class values, unlike types, aren't hoisted for that position. Track each class's declaration line during hoisting and compare against the extends site. Recorded rather than thrown so the deriving class body is still checked. Flips symbolProperty33 (symbolProperty34 still needs the TS2411 class-side symbol-index check, out of scope here). --- TypeSystem/TypeChecker.Statements.Classes.cs | 12 ++++++++++++ TypeSystem/TypeChecker.Statements.Functions.cs | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index 5ca561e2..493af013 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -60,6 +60,18 @@ private void CheckClassDeclaration(Stmt.Class classStmt) using (new EnvironmentScope(this, classTypeEnv)) superclass = ResolveDeclaredSuperclass(classStmt); + // `class C1 extends C2 {} class C2 {}` — a class value referenced in its own or an earlier + // class's extends clause, before that class is textually declared, is TS2449 (class values + // aren't hoisted for this position). Recorded (not thrown) so the body is still checked. + if (classStmt.SuperclassExpr is Expr.Variable baseVar + && _classDeclarationLines.TryGetValue(baseVar.Name.Lexeme, out int baseLine) + && baseLine > classStmt.Name.Line) + { + RecordTypeError(new TypeCheckException( + $"Class '{baseVar.Name.Lexeme}' used before its declaration.", + line: classStmt.Name.Line, tsCode: "TS2449")); + } + // Create mutable class early so self-references in method return types work. // This allows methods like "next(): Node" to correctly resolve the return type. // The mutable class is populated during signature collection and frozen at the end. diff --git a/TypeSystem/TypeChecker.Statements.Functions.cs b/TypeSystem/TypeChecker.Statements.Functions.cs index 31514f2a..9b8b5ec5 100644 --- a/TypeSystem/TypeChecker.Statements.Functions.cs +++ b/TypeSystem/TypeChecker.Statements.Functions.cs @@ -227,6 +227,13 @@ private void HoistLexicalDeclarations(IEnumerable statements) /// reference classes defined later in the same file. The real class type is defined when /// CheckClassDeclaration runs during the sequential pass, overwriting the Any placeholder. /// + /// + /// Source line of each class's textual declaration, keyed by name. Populated during hoisting so + /// a later extends clause can detect a forward reference (TS2449 — using a class before + /// its declaration; class values, unlike types, are not hoisted for that position). + /// + private readonly Dictionary _classDeclarationLines = []; + private void HoistClassDeclarations(IEnumerable statements) { foreach (var stmt in statements) @@ -234,10 +241,12 @@ private void HoistClassDeclarations(IEnumerable statements) switch (stmt) { case Stmt.Class cls: + _classDeclarationLines[cls.Name.Lexeme] = cls.Name.Line; if (!_environment.IsDefinedLocally(cls.Name.Lexeme)) _environment.Define(cls.Name.Lexeme, new TypeInfo.Any()); break; case Stmt.Export { Declaration: Stmt.Class exportCls }: + _classDeclarationLines[exportCls.Name.Lexeme] = exportCls.Name.Line; if (!_environment.IsDefinedLocally(exportCls.Name.Lexeme)) _environment.Define(exportCls.Name.Lexeme, new TypeInfo.Any()); break; From 8c775950e0d92729532a2a88ae78fd2a961b6ef2 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 19:26:28 -0700 Subject: [PATCH 20/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolProperty33=20now=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index a4597785..4bc99004 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -108,7 +108,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty3.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty30.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty31.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty32.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty33.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty33.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty34.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty35.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty36.ts Pass From 0425498601e653dc7c580ef6016dc61126d33ccc Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 20:03:02 -0700 Subject: [PATCH 21/32] fix(types): match symbol-keyed class fields structurally + assign objects to empty classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two structural-assignability gaps behind the symbol-keyed member tests: - A computed class field '[Symbol.iterator]: T' was keyed by the synthetic '' lexeme instead of its canonical '@@iterator' name (methods and accessors already canonicalize), so it never lined up with the same member on an interface — producing spurious missing-property errors. Route all field registration/lookup through a shared GetFieldMemberName helper. - An empty (member-less, index-less, unbranded) class target returned 'not assignable' for every source, but an object-like source (interface value / record) is assignable to '{}'. Add an opt-in flag so only the interface/record -> instance path relaxes; class-vs-class stays nominal. Flips symbolProperty9/10/11/12. --- .../TypeChecker.Compatibility.Helpers.cs | 9 ++++-- TypeSystem/TypeChecker.Compatibility.cs | 2 +- TypeSystem/TypeChecker.Statements.Classes.cs | 29 ++++++++++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/TypeSystem/TypeChecker.Compatibility.Helpers.cs b/TypeSystem/TypeChecker.Compatibility.Helpers.cs index 68601e95..78a34e9b 100644 --- a/TypeSystem/TypeChecker.Compatibility.Helpers.cs +++ b/TypeSystem/TypeChecker.Compatibility.Helpers.cs @@ -784,7 +784,8 @@ private Dictionary CollectGenericClassMembers(TypeInfo.Generic /// generic type arguments substituted. Returns false for branded or member-less/index-less targets /// (those stay nominal). /// - private bool StructurallyAssignableToClassTarget(TypeInfo targetResolved, TypeInfo source) + private bool StructurallyAssignableToClassTarget(TypeInfo targetResolved, TypeInfo source, + bool emptyTargetAcceptsObjectSource = false) { Dictionary members; bool hasIndex; @@ -813,7 +814,11 @@ private bool StructurallyAssignableToClassTarget(TypeInfo targetResolved, TypeIn default: return false; } - if (members.Count == 0 && !hasIndex) return false; + // A member-less, index-less unbranded target is structurally `{}`. For class-vs-class this + // stays nominal (caller passes false, preserving subclass-safety), but an object-like source + // (interface value / record) IS assignable to `{}` — that's the emptyTargetAcceptsObjectSource + // path, so `interface I → empty class C` no longer spuriously fails. + if (members.Count == 0 && !hasIndex) return emptyTargetAcceptsObjectSource; return CheckStructuralCompatibility(members, source) && IndexSignaturesSatisfied(indexCarrier, source); } diff --git a/TypeSystem/TypeChecker.Compatibility.cs b/TypeSystem/TypeChecker.Compatibility.cs index 2457dbfb..ddd9201b 100644 --- a/TypeSystem/TypeChecker.Compatibility.cs +++ b/TypeSystem/TypeChecker.Compatibility.cs @@ -987,7 +987,7 @@ actualIG.GenericDefinition is TypeInfo.GenericClass gc2 && // interface/record) are handled by the structural paths below. if (expected is TypeInfo.Instance targetInst && actual is TypeInfo.Interface or TypeInfo.Record && - StructurallyAssignableToClassTarget(targetInst.ResolvedClassType, actual)) + StructurallyAssignableToClassTarget(targetInst.ResolvedClassType, actual, emptyTargetAcceptsObjectSource: true)) { return true; } diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index 493af013..d424dcfe 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -25,6 +25,18 @@ public partial class TypeChecker _ => null }; + /// + /// The member-dictionary key for a class field: a well-known-symbol computed key + /// ([Symbol.iterator]) canonicalizes to its @@name so it matches the same member on + /// an interface/base class in structural comparison; everything else keeps its lexeme. Without this + /// a symbol-keyed field is stored under the synthetic <computed> lexeme and never lines + /// up with the interface's @@iterator, producing spurious missing-property errors. + /// + private static string GetFieldMemberName(Stmt.Field field) => + field.ComputedKey != null + ? (TryGetWellKnownSymbolMemberName(field.ComputedKey) ?? field.Name.Lexeme) + : field.Name.Lexeme; + private void CheckClassDeclaration(Stmt.Class classStmt) { // Check class decorators @@ -311,7 +323,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) DecoratorTarget fieldTarget = field.IsStatic ? DecoratorTarget.StaticField : DecoratorTarget.Field; CheckDecorators(field.Decorators, fieldTarget); - string fieldName = field.Name.Lexeme; + string fieldName = GetFieldMemberName(field); TypeInfo fieldType = ResolveAnnotation(field.TypeAnnotation, field.TypeAnnotationNode) ?? new TypeInfo.Any(); @@ -668,8 +680,8 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) TypeInfo initType = CheckExpr(field.Initializer); // For ES2022 private static fields, look in StaticPrivateFieldTypes TypeInfo staticFieldDeclaredType = field.IsPrivate - ? classTypeForBody.StaticPrivateFieldTypes[field.Name.Lexeme] - : classTypeForBody.StaticProperties[field.Name.Lexeme]; + ? classTypeForBody.StaticPrivateFieldTypes[GetFieldMemberName(field)] + : classTypeForBody.StaticProperties[GetFieldMemberName(field)]; if (!IsCompatible(staticFieldDeclaredType, initType)) { throw new TypeCheckException($" Cannot assign type '{initType}' to static property '{field.Name.Lexeme}' of type '{staticFieldDeclaredType}'.", tsCode: "TS2322"); @@ -766,7 +778,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) if (field.IsStatic || field.Initializer == null) continue; TypeInfo initType = CheckExpr(field.Initializer); var declaredTypes = field.IsPrivate ? classTypeForBody.PrivateFieldTypes : classTypeForBody.FieldTypes; - if (declaredTypes.TryGetValue(field.Name.Lexeme, out var fieldDeclaredType) + if (declaredTypes.TryGetValue(GetFieldMemberName(field), out var fieldDeclaredType) && fieldDeclaredType is not (TypeInfo.Inferred or TypeInfo.Any) && !IsCompatible(fieldDeclaredType, initType)) { @@ -1327,22 +1339,23 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // Collect field types foreach (var field in classStmt.Fields) { + string fieldName = GetFieldMemberName(field); TypeInfo fieldType = ResolveAnnotation(field.TypeAnnotation, field.TypeAnnotationNode) ?? new TypeInfo.Any(); if (field.IsStatic) { - mutableClass.StaticProperties[field.Name.Lexeme] = fieldType; + mutableClass.StaticProperties[fieldName] = fieldType; } else { - mutableClass.FieldTypes[field.Name.Lexeme] = fieldType; + mutableClass.FieldTypes[fieldName] = fieldType; } - (field.IsStatic ? mutableClass.StaticFieldAccess : mutableClass.FieldAccess)[field.Name.Lexeme] = field.Access; + (field.IsStatic ? mutableClass.StaticFieldAccess : mutableClass.FieldAccess)[fieldName] = field.Access; if (field.IsReadonly) { - mutableClass.ReadonlyFields.Add(field.Name.Lexeme); + mutableClass.ReadonlyFields.Add(fieldName); } } From a50114c1a048121431b3364639cb07f71e812ac2 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 20:03:02 -0700 Subject: [PATCH 22/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolProperty9/10/11/12=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index 4bc99004..6498d515 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -83,9 +83,9 @@ tests/cases/conformance/es6/Symbols/symbolDeclarationEmit7.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit8.ts Pass tests/cases/conformance/es6/Symbols/symbolDeclarationEmit9.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty1.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty10.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty11.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty12.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty10.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty11.ts Pass +tests/cases/conformance/es6/Symbols/symbolProperty12.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty13.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty14.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty15.ts Pass @@ -142,7 +142,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty60.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty61.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty7.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty8.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty9.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty9.ts Pass tests/cases/conformance/es6/Symbols/symbolType1.ts Fail tests/cases/conformance/es6/Symbols/symbolType10.ts Pass tests/cases/conformance/es6/Symbols/symbolType11.ts Pass From fe054a70332098623bac32ef6692c2df3811743f Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 20:15:37 -0700 Subject: [PATCH 23/32] fix(types): reject an interface extending two bases with a non-identical shared member TS2320: 'interface I3 extends I1, I2' is an error when I1 and I2 both declare the same member with types that aren't identical. Compare each pair of base members by mutual assignability (conservative, so a genuinely-shared diamond member never trips it) and report once at the interface name. Flips symbolProperty35. --- .../TypeChecker.Statements.Interfaces.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/TypeSystem/TypeChecker.Statements.Interfaces.cs b/TypeSystem/TypeChecker.Statements.Interfaces.cs index dd5c4760..ad555789 100644 --- a/TypeSystem/TypeChecker.Statements.Interfaces.cs +++ b/TypeSystem/TypeChecker.Statements.Interfaces.cs @@ -433,6 +433,32 @@ private void CheckInterfaceDeclaration(Stmt.Interface interfaceStmt) } finally { _extendsClauseConstraintLine = savedExtendsLine; } extends = extendsList.ToFrozenSet(); + + // TS2320: an interface may not simultaneously extend two bases that declare the same + // member with non-identical types. Two types are treated as "not identical" when they + // aren't mutually assignable — conservative, so a genuinely-shared (diamond) member, + // being mutually assignable, never trips it. + var seenBaseMembers = new Dictionary(); + bool reportedExtendConflict = false; + foreach (var baseItf in extendsList) + { + if (reportedExtendConflict) break; + foreach (var (mName, mType) in baseItf.GetAllMembers()) + { + if (seenBaseMembers.TryGetValue(mName, out var prev) + && prev.BaseName != baseItf.Name + && !(IsCompatible(prev.Type, mType) && IsCompatible(mType, prev.Type))) + { + string display = mName.StartsWith("@@") ? $"[Symbol.{mName[2..]}]" : mName; + RecordTypeError(new TypeCheckException( + $" Interface '{interfaceStmt.Name.Lexeme}' cannot simultaneously extend types '{prev.BaseName}' and '{baseItf.Name}'. Named property '{display}' of types '{prev.BaseName}' and '{baseItf.Name}' are not identical.", + line: interfaceStmt.Name.Line, tsCode: "TS2320")); + reportedExtendConflict = true; + break; + } + seenBaseMembers.TryAdd(mName, (baseItf.Name, mType)); + } + } } // Process call signatures From 3558c7947b2662251e2fa7626eb85e6da782af3c Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 20:15:37 -0700 Subject: [PATCH 24/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolProperty35=20now=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index 6498d515..ad0416b0 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -110,7 +110,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty31.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty32.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty33.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty34.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty35.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty35.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty36.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty37.ts TypeCheckError tests/cases/conformance/es6/Symbols/symbolProperty38.ts Pass From 402d53b434d96e1f477c856196e91c2c951fb332 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 20:52:22 -0700 Subject: [PATCH 25/32] fix(types): validate class symbol-keyed members against a symbol index signature TS2411: a class's symbol-keyed members (own and inherited) must be assignable to its effective '[s: symbol]' index type (own or inherited). Runs after method bodies so an un-annotated '[Symbol.x]()' carries its inferred return type. tsc's line attribution reproduced from all three cases: an own member reports at the member; an inherited member (paired with an own index signature) reports at the index signature; a fully-inherited pair is skipped (reported on the base). Flips symbolProperty30/32. symbolProperty34 stays deferred: its base class is declared after the derived one, so the forward-referenced base resolves to Any and its index signature isn't visible when the derived class is checked. --- TypeSystem/TypeChecker.Statements.Classes.cs | 6 ++ TypeSystem/TypeChecker.Validation.cs | 93 ++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index d424dcfe..e64d28c9 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -1091,6 +1091,12 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) _compatibilityCache = null; _identityCompatibilityCache = null; } + + // Symbol-index conformance (TS2411) runs HERE — after method bodies — so an un-annotated + // `[Symbol.x]()` method carries its inferred return type rather than the placeholder + // it held at the string-index check earlier (ValidateClassPropertiesAgainstIndex). + mutableClass.ResetFrozenCache(); + ValidateClassMembersAgainstSymbolIndex(classStmt, mutableClass.Freeze()); } /// diff --git a/TypeSystem/TypeChecker.Validation.cs b/TypeSystem/TypeChecker.Validation.cs index 33d461f8..26288096 100644 --- a/TypeSystem/TypeChecker.Validation.cs +++ b/TypeSystem/TypeChecker.Validation.cs @@ -553,6 +553,99 @@ private void ValidateClassPropertiesAgainstIndex(Stmt.Class classStmt, TypeInfo. } } } + /// + /// Validates that a class's symbol-keyed members (own and inherited) are assignable to its + /// effective [s: symbol] index type (own or inherited) — TS2411, the symbol-index analogue + /// of . Runs after method bodies so inferred + /// return types are resolved. tsc attributes the diagnostic to whichever of {member, index + /// signature} is declared ON this class: an OWN member reports at the member, an INHERITED member + /// (necessarily paired with an OWN index signature) reports at the index signature. A member+index + /// pair both inherited from the same base was already reported on that base and is skipped. + /// + private void ValidateClassMembersAgainstSymbolIndex(Stmt.Class classStmt, TypeInfo.Class classType) + { + TypeInfo? ownSymIndex = classType.SymbolIndexType; + TypeInfo? effectiveIndex = ownSymIndex ?? FindSymbolIndexInBaseChain(classType.Superclass); + if (effectiveIndex is null) return; + + void CheckOwnMember(string atName, TypeInfo memberType, int line) + { + if (!IsCompatible(effectiveIndex, memberType)) + RecordTypeError(new TypeCheckException( + $" Property '[Symbol.{atName[2..]}]' of type '{memberType}' is not assignable to 'symbol' index type '{effectiveIndex}'.", + line: line, tsCode: "TS2411")); + } + + // Own symbol-keyed members: methods and fields whose computed key is a well-known symbol. + var ownNames = new HashSet(); + foreach (var method in classStmt.Methods) + { + if (method.ComputedKey is null) continue; + string? name = TryGetWellKnownSymbolMemberName(method.ComputedKey); + if (name is null || !classType.Methods.TryGetValue(name, out var mType)) continue; + ownNames.Add(name); + CheckOwnMember(name, mType, TryGetExprLine(method.ComputedKey) ?? method.Name.Line); + } + foreach (var field in classStmt.Fields) + { + if (field.IsStatic || field.ComputedKey is null) continue; + string? name = TryGetWellKnownSymbolMemberName(field.ComputedKey); + if (name is null || !classType.FieldTypes.TryGetValue(name, out var fType)) continue; + ownNames.Add(name); + CheckOwnMember(name, fType, TryGetExprLine(field.ComputedKey) ?? field.Name.Line); + } + + // Inherited symbol-keyed members are checked only when the index signature is OWN to this + // class (otherwise the member+index pair is fully inherited and the base already reported). + // They are attributed to this class's index-signature line. + if (ownSymIndex is not null) + { + int? indexLine = classStmt.IndexSignatures? + .FirstOrDefault(s => s.KeyType == TokenType.TYPE_SYMBOL)?.KeyName.Line; + foreach (var (name, mType) in CollectInheritedSymbolMembers(classType.Superclass)) + { + if (ownNames.Contains(name)) continue; // overridden here → own-member path handled it + if (!IsCompatible(ownSymIndex, mType)) + RecordTypeError(new TypeCheckException( + $" Property '[Symbol.{name[2..]}]' of type '{mType}' is not assignable to 'symbol' index type '{ownSymIndex}'.", + line: indexLine, tsCode: "TS2411")); + } + } + } + + private static TypeInfo? GetSymbolIndexType(TypeInfo? classType) => classType switch + { + TypeInfo.Class c => c.SymbolIndexType, + TypeInfo.InstantiatedGeneric { GenericDefinition: TypeInfo.GenericClass gc } => gc.SymbolIndexType, + TypeInfo.GenericClass g => g.SymbolIndexType, + _ => null + }; + + private TypeInfo? FindSymbolIndexInBaseChain(TypeInfo? baseType) + { + for (TypeInfo? current = baseType; current != null; current = GetSuperclass(current)) + if (GetSymbolIndexType(current) is { } idx) return idx; + return null; + } + + /// Symbol-keyed (@@name) methods and fields inherited from the base-class chain, + /// each yielded once (nearest override wins). + private IEnumerable<(string Name, TypeInfo Type)> CollectInheritedSymbolMembers(TypeInfo? baseType) + { + var seen = new HashSet(); + for (TypeInfo? current = baseType; current != null; current = GetSuperclass(current)) + { + if (GetMethods(current) is { } methods) + foreach (var kv in methods) + if (kv.Key.StartsWith("@@") && seen.Add(kv.Key)) + yield return (kv.Key, kv.Value); + if (GetFieldTypes(current) is { } fields) + foreach (var kv in fields) + if (kv.Key.StartsWith("@@") && seen.Add(kv.Key)) + yield return (kv.Key, kv.Value); + } + } + /// /// Walks the base-class chain looking for the accessibility of a field, getter or method named /// . Returns its , or null if absent. From 264d87a42df2b3a5edf3a0a2ddd44343ec8c1a81 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 20:52:22 -0700 Subject: [PATCH 26/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolProperty30/32=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index ad0416b0..a9aa445d 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -105,9 +105,9 @@ tests/cases/conformance/es6/Symbols/symbolProperty27.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty28.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty29.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty3.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty30.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty30.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty31.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty32.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty32.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty33.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty34.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty35.ts Pass From 8b1efd4da982c42bd86fd9ac09ecae4ef4c270b8 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 21:11:51 -0700 Subject: [PATCH 27/32] fix(types): accept a boolean argument to BigInt() BigInt's parameter type is 'string | number | bigint | boolean' (a boolean coerces to 0n/1n); the checker rejected boolean. Flips constructBigint. --- TypeSystem/TypeChecker.Calls.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/TypeSystem/TypeChecker.Calls.cs b/TypeSystem/TypeChecker.Calls.cs index a76696b4..730ddc27 100644 --- a/TypeSystem/TypeChecker.Calls.cs +++ b/TypeSystem/TypeChecker.Calls.cs @@ -417,9 +417,11 @@ private bool TryCheckBuiltinCall(Expr.Call call, out TypeInfo result) throw new TypeCheckException("BigInt() requires exactly one argument.", tsCode: "TS2554"); } var argType = CheckExpr(call.Arguments[0]); - if (!IsNumber(argType) && !IsString(argType) && !IsBigInt(argType) && argType is not TypeInfo.Any) + // BigInt's parameter type is `string | number | bigint | boolean` (a boolean coerces to 0n/1n). + bool isBoolean = argType is TypeInfo.Primitive { Type: TokenType.TYPE_BOOLEAN } or TypeInfo.BooleanLiteral; + if (!IsNumber(argType) && !IsString(argType) && !IsBigInt(argType) && !isBoolean && argType is not TypeInfo.Any) { - throw new TypeCheckException($"BigInt() argument must be a number, string, or bigint, got '{argType}'.", tsCode: "TS2345"); + throw new TypeCheckException($"BigInt() argument must be a number, string, bigint, or boolean, got '{argType}'.", tsCode: "TS2345"); } { result = new TypeInfo.BigInt(); return true; } } From a312fe8fe737043365a02b64da1fcc3e18d2c735 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 21:11:51 -0700 Subject: [PATCH 28/32] fix(types): relate an unconstrained type parameter to a no-required-member target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unconstrained type parameter has no apparent type, so it was assignable to nothing. tsc still relates it to an all-optional / empty object target (its 'subtyping assumes transitivity for optional properties' allowance) — but not to a primitive, 'object', or a target with any required member. Flips subtypingWithOptionalProperties. --- .../TypeChecker.Compatibility.Relations.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/TypeSystem/TypeChecker.Compatibility.Relations.cs b/TypeSystem/TypeChecker.Compatibility.Relations.cs index f2975d08..7645b91c 100644 --- a/TypeSystem/TypeChecker.Compatibility.Relations.cs +++ b/TypeSystem/TypeChecker.Compatibility.Relations.cs @@ -93,7 +93,22 @@ private bool TryRelateTypeParameters(TypeInfo expected, TypeInfo actual, out boo return true; } var apparent = ApparentTypeOf(actualTpOnly); - result = apparent != null && IsCompatible(expected, apparent); + if (apparent == null) + { + // An unconstrained type parameter has no apparent (constraint) type. tsc still relates + // it to a target that requires NO members — an all-optional / empty object type — its + // "subtyping assumes transitivity for optional properties (99% case)" allowance. It is + // NOT assignable to a primitive, `object`, or any target with a required member. + result = expected switch + { + TypeInfo.Record rec => HasNoRequiredMembers(rec), + TypeInfo.Interface itf => !itf.HasIndexSignature && !itf.IsCallable && !itf.IsConstructable + && itf.GetAllMembers().All(m => itf.GetAllOptionalMembers().Contains(m.Key)), + _ => false + }; + return true; + } + result = IsCompatible(expected, apparent); return true; } result = false; From 2312d94c20a72fbe8f1e2a35547d9f3c7f3fe434 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 21:11:51 -0700 Subject: [PATCH 29/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20constructBigint=20+=20subtypingWithOptionalProperti?= =?UTF-8?q?es=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index a9aa445d..fa7097d2 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -39,7 +39,7 @@ tests/cases/conformance/es2019/importMeta/importMetaNarrowing.ts Fail tests/cases/conformance/es2020/bigintMissingES2019.ts Pass tests/cases/conformance/es2020/bigintMissingES2020.ts Pass tests/cases/conformance/es2020/bigintMissingESNext.ts Pass -tests/cases/conformance/es2020/constructBigint.ts Fail +tests/cases/conformance/es2020/constructBigint.ts Pass tests/cases/conformance/es2020/es2020IntlAPIs.ts Fail tests/cases/conformance/es2020/intlNumberFormatES2020.ts Pass tests/cases/conformance/es2020/localesObjectArgument.ts Pass @@ -288,7 +288,7 @@ tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingW tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithObjectMembersOptionality2.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithObjectMembersOptionality3.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithObjectMembersOptionality4.ts Pass -tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithOptionalProperties.ts Fail +tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithOptionalProperties.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithStringIndexer.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithStringIndexer2.ts Pass tests/cases/conformance/types/typeRelationships/subtypesAndSuperTypes/subtypingWithStringIndexer3.ts Pass From a532c22e17e2e8bc70f742bd8d6e812c4c48cbe1 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 21:34:09 -0700 Subject: [PATCH 30/32] fix(types): reject a plain-symbol computed class field name (TS1166) A computed DATA-property name on a class (unlike a method/accessor) must be a literal type or a 'unique symbol'; a plain non-unique 'symbol' (e.g. [Symbol()]) is not allowed, while a well-known symbol ([Symbol.iterator], a unique symbol) is fine. Flips symbolProperty7. --- TypeSystem/TypeChecker.Statements.Classes.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/TypeSystem/TypeChecker.Statements.Classes.cs b/TypeSystem/TypeChecker.Statements.Classes.cs index e64d28c9..b13590aa 100644 --- a/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/TypeSystem/TypeChecker.Statements.Classes.cs @@ -327,6 +327,16 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) TypeInfo fieldType = ResolveAnnotation(field.TypeAnnotation, field.TypeAnnotationNode) ?? new TypeInfo.Any(); + // TS1166: a computed DATA-property name (a class field, unlike a method/accessor) must be a + // literal type or a 'unique symbol'. A plain, non-unique `symbol` — e.g. `[Symbol()]` — + // is not allowed; a well-known symbol (`[Symbol.iterator]`) is a unique symbol, so it's fine. + if (field.ComputedKey != null && CheckExpr(field.ComputedKey) is TypeInfo.Symbol) + { + RecordTypeError(new TypeCheckException( + "A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type.", + line: TryGetExprLine(field.ComputedKey) ?? field.Name.Line, tsCode: "TS1166")); + } + // Handle ES2022 private fields (#field) if (field.IsPrivate) { From 0b7b75409d9dc29c197de4727bc49c0cc0173548 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 21:34:09 -0700 Subject: [PATCH 31/32] fix(types): report TS2538 when indexing with a function-typed key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A value whose type can't be a property key — e.g. a function type like the 'Symbol.for' method — cannot be used as an index (TS2538), distinct from the implicit-any TS7053 used for a valid-but-unmodeled key. Flips symbolProperty53. --- TypeSystem/TypeChecker.Properties.Index.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/TypeSystem/TypeChecker.Properties.Index.cs b/TypeSystem/TypeChecker.Properties.Index.cs index 41e2cf15..0edf5023 100644 --- a/TypeSystem/TypeChecker.Properties.Index.cs +++ b/TypeSystem/TypeChecker.Properties.Index.cs @@ -33,6 +33,15 @@ private TypeInfo CheckGetIndex(Expr.GetIndex getIndex) return new TypeInfo.Any(); } + // TS2538: a value whose type can't be a property key — e.g. a function type such as + // `Symbol.for` (the method, not a call) — cannot be used as an index. This is distinct from + // the implicit-any TS7053 emitted below for a valid-but-unmodeled key. + if (indexType is TypeInfo.Function or TypeInfo.OverloadedFunction) + { + throw new TypeCheckException($" Type '{indexType}' cannot be used as an index type.", + line: TryGetExprLine(getIndex.Index), tsCode: "TS2538"); + } + // Indexing a method's inferred-return-type placeholder (still being resolved during // body checking, e.g. calling a sibling static method from a ternary). Treat as any. if (objType is TypeInfo.Inferred) From bde494acc8896b8b914b656f1c315a8b0229de5e Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Wed, 8 Jul 2026 21:34:10 -0700 Subject: [PATCH 32/32] =?UTF-8?q?test(conformance):=20update=20baseline=20?= =?UTF-8?q?=E2=80=94=20symbolProperty7/53=20now=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SharpTS.TypeScriptConformance/baselines/interpreted.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SharpTS.TypeScriptConformance/baselines/interpreted.txt b/SharpTS.TypeScriptConformance/baselines/interpreted.txt index fa7097d2..a9f1658e 100644 --- a/SharpTS.TypeScriptConformance/baselines/interpreted.txt +++ b/SharpTS.TypeScriptConformance/baselines/interpreted.txt @@ -130,7 +130,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty5.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty50.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty51.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty52.ts Pass -tests/cases/conformance/es6/Symbols/symbolProperty53.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty53.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty54.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty55.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty56.ts Pass @@ -140,7 +140,7 @@ tests/cases/conformance/es6/Symbols/symbolProperty59.ts Fail tests/cases/conformance/es6/Symbols/symbolProperty6.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty60.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty61.ts Fail -tests/cases/conformance/es6/Symbols/symbolProperty7.ts Fail +tests/cases/conformance/es6/Symbols/symbolProperty7.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty8.ts Pass tests/cases/conformance/es6/Symbols/symbolProperty9.ts Pass tests/cases/conformance/es6/Symbols/symbolType1.ts Fail