fix: nested types remaining fixes (#591–#594) - #595
Conversation
Add Decorators property to EnumDef AST node, enable parser to accept decorators on enum definitions, wire up NameResolver to read access decorators from enums, and apply private-by-default to nested enums in codegen (matching ClassDef/StructDef/InterfaceDef behavior). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract LookupNestedType helper in TypeResolver for dotted-name resolution (e.g., Registry.Entry), and add it as a fallback in ResolveGenericType so Registry.Entry[int] resolves correctly. Also add codegen support for generic nested type constructor calls (Outer.Inner[T](...)) by walking the TypeSymbol.NestedTypes chain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…members (#593) Add IsNestedWithin check to AccessValidator so nested types can access their enclosing type's private and protected members, matching C# semantics. Also fix nested type lookup during validation by falling back to CurrentClass.NestedTypes when SymbolTable.LookupType fails (nested types are defined in class scopes which are popped after NameResolver completes). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add NestedTypes property to CachedSymbol, serialize TypeSymbol.NestedTypes as inline embedded CachedSymbols, restore DeclaringType from parent-child structure during deserialization, and recursively resolve nested types' BaseType/Interfaces in ResolveReferences. Bump schema version 10 → 11. Also add VisitEnumDef to DecoratorValidator to validate decorators on enums (catches @DataClass on enum with proper error message). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes remaining gaps in nested type support across parsing, semantic validation/resolution, codegen, and incremental compilation caching.
Changes:
- Added decorator support for
EnumDefand updated codegen so nested enums default toprivateunless explicitly decorated. - Improved semantic resolution for dotted-name nested types (including generic nested types) and updated access validation to allow nested types to access enclosing private/protected members.
- Implemented serialization/deserialization of
TypeSymbol.NestedTypes(and wiring ofDeclaringType) and bumped incremental cache schema version.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Sharpy.Compiler/Semantic/Validation/DecoratorValidator.cs | Validates decorators on enums (and dataclass-on-enum error path). |
| src/Sharpy.Compiler/Semantic/Validation/AccessValidator.cs | Allows nested types to access enclosing private/protected members; improves nested type traversal symbol lookup. |
| src/Sharpy.Compiler/Semantic/TypeResolver.cs | Adds dotted-name nested-type lookup for both non-generic and generic type annotations. |
| src/Sharpy.Compiler/Semantic/NameResolver.Declarations.cs | Includes EnumDef decorators when determining nested type access. |
| src/Sharpy.Compiler/Project/SymbolSerializer.cs | Serializes/deserializes nested types and recursively resolves their references; bumps cache behavior. |
| src/Sharpy.Compiler/Project/SymbolCache.cs | Extends cached symbol schema with NestedTypes. |
| src/Sharpy.Compiler/Project/IncrementalCompilationCache.cs | Bumps cache schema version (10 → 11). |
| src/Sharpy.Compiler/Parser/Parser.cs | Allows decorators on enum statements and updates related parser diagnostics. |
| src/Sharpy.Compiler/Parser/Ast/Statement.cs | Adds Decorators to EnumDef AST node. |
| src/Sharpy.Compiler/CodeGen/RoslynEmitter.Expressions.Access.cs | Adds codegen path for Outer.Inner[T](...) nested generic instantiation. |
| src/Sharpy.Compiler/CodeGen/RoslynEmitter.ClassMembers.cs | Applies private-by-default behavior for nested enums unless explicitly decorated. |
| src/Sharpy.Compiler.Tests/Project/IncrementalCompilationTests.cs | Adds serializer round-trip test for nested types. |
| src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/* | Adds/updates nested-type integration fixtures; unskips nested generic test. |
| src/Sharpy.Compiler.Tests/Integration/TestFixtures/errors/dataclass_on_enum.error | Updates expected error message for @dataclass on enums. |
| foreach (var nested in nestedTypes) | ||
| { | ||
| nested.DeclaringType = symbol; | ||
| symbolRegistry[ComputeSymbolId(nested, nested.DefiningFilePath ?? cached.FilePath)] = nested; |
There was a problem hiding this comment.
Registering nested types into symbolRegistry using ComputeSymbolId(nested, ...) inherits the same {file}:{kind}:{name} collision risk for nested types. If multiple nested types share a name within the same file, later ones will overwrite earlier ones and ResolveReferences may bind to the wrong symbol. Once IDs are made fully-qualified, this registration should use the fully-qualified ID as well.
| foreach (var nested in nestedTypes) | |
| { | |
| nested.DeclaringType = symbol; | |
| symbolRegistry[ComputeSymbolId(nested, nested.DefiningFilePath ?? cached.FilePath)] = nested; | |
| for (var i = 0; i < nestedTypes.Count; i++) | |
| { | |
| var nested = nestedTypes[i]; | |
| nested.DeclaringType = symbol; | |
| var nestedCached = cached.NestedTypes?[i]; | |
| var nestedSymbolId = !string.IsNullOrEmpty(nestedCached?.Id) | |
| ? nestedCached.Id | |
| : ComputeSymbolId(nested, nested.DefiningFilePath ?? cached.FilePath); | |
| symbolRegistry[nestedSymbolId] = nested; |
| private static bool IsNestedWithin(TypeSymbol? currentClass, TypeSymbol owningType) | ||
| { | ||
| var declaring = currentClass?.DeclaringType; | ||
| while (declaring != null) | ||
| { | ||
| if (ReferenceEquals(declaring, owningType) || declaring.Name == owningType.Name) | ||
| return true; | ||
| declaring = declaring.DeclaringType; | ||
| } | ||
| return false; |
There was a problem hiding this comment.
IsNestedWithin falls back to declaring.Name == owningType.Name. For nested-type access checks this is likely too permissive (nested types named Inner/Helper are common), and can incorrectly grant private/protected access across unrelated types that happen to share a name. Prefer strict identity (ReferenceEquals) or a stronger stable identity check (e.g., include defining file/module + declaring chain) rather than name-only matching.
| public override void VisitEnumDef(EnumDef node) | ||
| { | ||
| ValidateDecorators(node.Decorators, node.Name); | ||
| ValidateDataclassOnNonClass(node.Decorators, node.Name, "enum"); | ||
| base.VisitEnumDef(node); | ||
| } |
There was a problem hiding this comment.
VisitEnumDef validates unsupported decorators and @dataclass, but it doesn't validate access-modifier decorator conflicts (e.g., @public + @private) the way member definitions do via ValidateAccessModifierDecorators. Since enums now support decorators, it would be good to reject conflicting access modifiers here (and ideally for other type definitions for consistency).
| var typeArgsSyntax = _typeMapper.MapTypeArgumentsFromExpression(indexAccess.Index); | ||
| var csharpName = NameMangler.ToPascalCase(memberAccess.Member); | ||
| var outerName = GetNestedTypeOuterPrefix(nestedTypeSymbol); | ||
| var qualifiedGenericName = QualifiedName( | ||
| ParseName(outerName), | ||
| GenericName(csharpName) | ||
| .WithTypeArgumentList(TypeArgumentList(SeparatedList(typeArgsSyntax)))); | ||
|
|
There was a problem hiding this comment.
The nested generic instantiation path uses NameMangler.ToPascalCase and a string-based outerName + ParseName(...) to build the type name. This can diverge from existing nested-type codegen (BuildNestedTypeName) which uses NameMangler.Transform(..., NameContext.Type) to handle keywords/backtick-escaped identifiers consistently. Consider reusing BuildNestedTypeName (or the same NameMangler.Transform logic) when forming qualifiedGenericName so nested generic types emit the same mangled names as non-generic nested types.
| // Serialize nested types as nested CachedSymbols | ||
| List<CachedSymbol>? nestedTypes = null; | ||
| if (ts.NestedTypes.Count > 0) | ||
| { | ||
| nestedTypes = ts.NestedTypes.Select(nt => | ||
| SerializeTypeSymbol(nt, ComputeSymbolId(nt, nt.DefiningFilePath ?? filePath), nt.DefiningFilePath ?? filePath)).ToList(); | ||
| } |
There was a problem hiding this comment.
ComputeSymbolId is based on {file}:{kind}:{name}; when serializing nested types this can collide for common nested names (e.g., OuterA.Inner and OuterB.Inner in the same file both become ...:Type:Inner). That will overwrite entries in symbolRegistry and make BaseType/Interface/NestedTypes reference resolution nondeterministic for incremental compilation. Consider including the declaring type chain (e.g., OuterA.Inner) or another stable fully-qualified symbol path in the ID for nested types (and use that consistently for BaseTypeId/InterfaceEntries as well).
Summary
EnumDef— nested enums now default toprivate(Axiom 1), with@public/@protectedoverridesRegistry.Entry[int])TypeSymbol.NestedTypesandDeclaringTypeinSymbolSerializerfor incremental compilation (schema version 10 → 11)Closes #591, closes #592, closes #593, closes #594
Test plan
nested_enum_public.spy— nested enum with@publicdecorator accessible from outsidenested_access_enclosing_private.spy— nested class accessing__-prefixed private membernested_access_enclosing_protected.spy— nested class accessing_-prefixed protected membernested_generic.spy— generic nested typeRegistry.Entry[int]resolves and runs (unskipped)SymbolSerializer_RoundTrip_NestedTypes— serialization round-trip preserves nested types and DeclaringTypedataclass_on_enumerror test updated for validator-level rejection🤖 Generated with Claude Code