From df3dce1142f58197114778f359a3ebdb21ba8705 Mon Sep 17 00:00:00 2001 From: antonsynd Date: Thu, 23 Apr 2026 22:44:36 -0400 Subject: [PATCH 1/4] feat: add decorator support to EnumDef for access modifiers (#592) 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) --- .../TestFixtures/nested_types/nested_enum.spy | 1 + .../nested_types/nested_enum_public.expected | 3 +++ .../nested_types/nested_enum_public.spy | 22 +++++++++++++++++++ .../CodeGen/RoslynEmitter.ClassMembers.cs | 5 ++++- src/Sharpy.Compiler/Parser/Ast/Statement.cs | 2 ++ src/Sharpy.Compiler/Parser/Parser.cs | 8 ++++--- .../Semantic/NameResolver.Declarations.cs | 1 + 7 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum_public.expected create mode 100644 src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum_public.spy diff --git a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum.spy b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum.spy index 203c666b7..82f41d61b 100644 --- a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum.spy +++ b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum.spy @@ -1,4 +1,5 @@ class TrafficLight: + @public enum State: Red = 1 Yellow = 2 diff --git a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum_public.expected b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum_public.expected new file mode 100644 index 000000000..f40b66db4 --- /dev/null +++ b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum_public.expected @@ -0,0 +1,3 @@ +Blue +Square +Green diff --git a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum_public.spy b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum_public.spy new file mode 100644 index 000000000..8cb33aa6f --- /dev/null +++ b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_enum_public.spy @@ -0,0 +1,22 @@ +class Container: + @public + enum Color: + Red = 1 + Blue = 2 + Green = 3 + + enum Shape: + Circle = 1 + Square = 2 + + def show_color(self) -> str: + return str(Container.Color.Blue) + + def show_shape(self) -> str: + return str(Container.Shape.Square) + +def main(): + c: Container = Container() + print(c.show_color()) + print(c.show_shape()) + print(Container.Color.Green) diff --git a/src/Sharpy.Compiler/CodeGen/RoslynEmitter.ClassMembers.cs b/src/Sharpy.Compiler/CodeGen/RoslynEmitter.ClassMembers.cs index 4c71dfb3d..3d3828e3c 100644 --- a/src/Sharpy.Compiler/CodeGen/RoslynEmitter.ClassMembers.cs +++ b/src/Sharpy.Compiler/CodeGen/RoslynEmitter.ClassMembers.cs @@ -210,10 +210,13 @@ private List GenerateClassMembers( break; case EnumDef nestedEnum: - // EnumDef lacks Decorators — keep public until parser supports access decorators on enums (#592) var nestedEnumNode = GenerateEnumDeclaration(nestedEnum); if (nestedEnumNode is MemberDeclarationSyntax nestedEnumMember) + { + if (!HasExplicitAccessDecorator(nestedEnum.Decorators)) + nestedEnumMember = ReplaceAccessModifier(nestedEnumMember, SyntaxKind.PrivateKeyword); members.Add(nestedEnumMember); + } break; default: diff --git a/src/Sharpy.Compiler/Parser/Ast/Statement.cs b/src/Sharpy.Compiler/Parser/Ast/Statement.cs index 49888b8ad..6c3a5499e 100644 --- a/src/Sharpy.Compiler/Parser/Ast/Statement.cs +++ b/src/Sharpy.Compiler/Parser/Ast/Statement.cs @@ -604,6 +604,7 @@ public record EnumDef : Statement public bool IsNameBacktickEscaped { get; init; } public ImmutableArray Members { get; init; } = ImmutableArray.Empty; public string? DocString { get; init; } + public ImmutableArray Decorators { get; init; } = ImmutableArray.Empty; /// public override void ValidateInvariants() @@ -611,6 +612,7 @@ public override void ValidateInvariants() base.ValidateInvariants(); Debug.Assert(!string.IsNullOrEmpty(Name), "EnumDef.Name cannot be null or empty"); Debug.Assert(Members != null, "EnumDef.Members cannot be null"); + Debug.Assert(Decorators != null, "EnumDef.Decorators cannot be null"); } /// diff --git a/src/Sharpy.Compiler/Parser/Parser.cs b/src/Sharpy.Compiler/Parser/Parser.cs index 82450371b..a64cbc1ca 100644 --- a/src/Sharpy.Compiler/Parser/Parser.cs +++ b/src/Sharpy.Compiler/Parser/Parser.cs @@ -499,11 +499,12 @@ private Statement ParseDecoratedStatement() TokenType.Struct => ParseStructDef(), TokenType.Interface => ParseInterfaceDef(), TokenType.Union => ParseUnionDef(), + TokenType.Enum => ParseEnumDef(), TokenType.Property => ParsePropertyDef(), TokenType.Event => ParseEventDef(), // Allow decorators on variable declarations (e.g., @static field in class body) TokenType.Identifier => ParseSimpleStatement(), - _ => throw ReportError("Decorators can only be applied to functions, classes, structs, interfaces, properties, events, or field declarations", Current.Line, Current.Column, DiagnosticCodes.Parser.InvalidDecoratorTarget, span: CurrentSpan) + _ => throw ReportError("Decorators can only be applied to functions, classes, structs, interfaces, enums, properties, events, or field declarations", Current.Line, Current.Column, DiagnosticCodes.Parser.InvalidDecoratorTarget, span: CurrentSpan) }; } finally @@ -519,11 +520,12 @@ private Statement ParseDecoratedStatement() StructDef str => str with { Decorators = decorators.ToImmutableArray() }, InterfaceDef iface => iface with { Decorators = decorators.ToImmutableArray() }, UnionDef union => union with { Decorators = decorators.ToImmutableArray() }, + EnumDef en => en with { Decorators = decorators.ToImmutableArray() }, PropertyDef prop => prop with { Decorators = decorators.ToImmutableArray() }, EventDef ev => ev with { Decorators = decorators.ToImmutableArray() }, VariableDeclaration varDecl => varDecl with { Decorators = decorators.ToImmutableArray() }, - Assignment => throw ReportError("Decorators cannot be applied to assignments — only functions, classes, structs, interfaces, properties, events, or field declarations", stmt.LineStart, stmt.ColumnStart, DiagnosticCodes.Parser.InvalidDecoratorTarget, span: stmt.Span), - _ => throw ReportError("Decorators can only be applied to functions, classes, structs, interfaces, properties, events, or field declarations", stmt.LineStart, stmt.ColumnStart, DiagnosticCodes.Parser.InvalidDecoratorTarget, span: stmt.Span) + Assignment => throw ReportError("Decorators cannot be applied to assignments — only functions, classes, structs, interfaces, enums, properties, events, or field declarations", stmt.LineStart, stmt.ColumnStart, DiagnosticCodes.Parser.InvalidDecoratorTarget, span: stmt.Span), + _ => throw ReportError("Decorators can only be applied to functions, classes, structs, interfaces, enums, properties, events, or field declarations", stmt.LineStart, stmt.ColumnStart, DiagnosticCodes.Parser.InvalidDecoratorTarget, span: stmt.Span) }; } diff --git a/src/Sharpy.Compiler/Semantic/NameResolver.Declarations.cs b/src/Sharpy.Compiler/Semantic/NameResolver.Declarations.cs index ab65f9443..353f7cc6d 100644 --- a/src/Sharpy.Compiler/Semantic/NameResolver.Declarations.cs +++ b/src/Sharpy.Compiler/Semantic/NameResolver.Declarations.cs @@ -516,6 +516,7 @@ private void ResolveNestedTypeDeclaration(Statement statement, TypeSymbol enclos ClassDef c => c.Decorators, StructDef s => s.Decorators, InterfaceDef i => i.Decorators, + EnumDef e => e.Decorators, _ => System.Collections.Immutable.ImmutableArray.Empty }; From 2feaeef006b7ca1cb32fe960b8848d4a2d7ce6c1 Mon Sep 17 00:00:00 2001 From: antonsynd Date: Thu, 23 Apr 2026 22:49:34 -0400 Subject: [PATCH 2/4] fix: resolve generic nested types via dotted-name lookup (#594) 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) --- .../nested_types/nested_generic.skip | 1 - .../RoslynEmitter.Expressions.Access.cs | 59 +++++++++++++++++++ src/Sharpy.Compiler/Semantic/TypeResolver.cs | 41 +++++++------ 3 files changed, 82 insertions(+), 19 deletions(-) delete mode 100644 src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_generic.skip diff --git a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_generic.skip b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_generic.skip deleted file mode 100644 index f52e8c3ba..000000000 --- a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_generic.skip +++ /dev/null @@ -1 +0,0 @@ -Generic nested types not yet resolved in type system (#594) diff --git a/src/Sharpy.Compiler/CodeGen/RoslynEmitter.Expressions.Access.cs b/src/Sharpy.Compiler/CodeGen/RoslynEmitter.Expressions.Access.cs index 931728734..43c68db6a 100644 --- a/src/Sharpy.Compiler/CodeGen/RoslynEmitter.Expressions.Access.cs +++ b/src/Sharpy.Compiler/CodeGen/RoslynEmitter.Expressions.Access.cs @@ -26,6 +26,15 @@ private ExpressionSyntax GenerateCall(FunctionCall call) return result; } + // Handle generic nested type instantiation: Outer.Inner[int](42) + if (call.Function is IndexAccess nestedIndexAccess && + nestedIndexAccess.Object is MemberAccess nestedMemberAccess) + { + var result = GenerateNestedGenericInstantiation(nestedIndexAccess, nestedMemberAccess, call); + if (result != null) + return result; + } + if (call.Function is Identifier funcName) { // Check if this is a builtin function call (e.g., int(), str(), print(), len(), etc.) @@ -492,6 +501,56 @@ symbol is TypeSymbol typeSymbol && return null; } + private ExpressionSyntax? GenerateNestedGenericInstantiation( + IndexAccess indexAccess, MemberAccess memberAccess, FunctionCall call) + { + var nestedTypeSymbol = LookupNestedTypeFromMemberAccess(memberAccess); + if (nestedTypeSymbol == null || !nestedTypeSymbol.IsGeneric) + return null; + + 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)))); + + var constructorTarget = ResolveConstructorForCall(nestedTypeSymbol, call); + var allArgs = GenerateReorderedCallArguments(call, constructorTarget); + + return ObjectCreationExpression(qualifiedGenericName) + .WithArgumentList(ArgumentList(SeparatedList(allArgs))); + } + + private TypeSymbol? LookupNestedTypeFromMemberAccess(MemberAccess memberAccess) + { + if (memberAccess.Object is Identifier outerName) + { + var outerSymbol = _context.LookupSymbol(outerName.Name) as TypeSymbol; + return outerSymbol?.NestedTypes.FirstOrDefault(n => n.Name == memberAccess.Member); + } + if (memberAccess.Object is MemberAccess innerAccess) + { + var parentType = LookupNestedTypeFromMemberAccess(innerAccess); + return parentType?.NestedTypes.FirstOrDefault(n => n.Name == memberAccess.Member); + } + return null; + } + + private static string GetNestedTypeOuterPrefix(TypeSymbol nestedType) + { + var parts = new List(); + var declaring = nestedType.DeclaringType; + while (declaring != null) + { + parts.Add(NameMangler.ToPascalCase(declaring.Name)); + declaring = declaring.DeclaringType; + } + parts.Reverse(); + return string.Join(".", parts); + } + /// /// For DefaultDict construction, wraps type-reference arguments in factory lambdas. /// defaultdict[str, list[int]](list) becomes diff --git a/src/Sharpy.Compiler/Semantic/TypeResolver.cs b/src/Sharpy.Compiler/Semantic/TypeResolver.cs index 0337f74f9..f33b65435 100644 --- a/src/Sharpy.Compiler/Semantic/TypeResolver.cs +++ b/src/Sharpy.Compiler/Semantic/TypeResolver.cs @@ -168,23 +168,8 @@ public SemanticType ResolveTypeAnnotation(TypeAnnotation? annotation) // Look up user-defined type else { - var typeSymbol = _symbolTable.LookupType(annotation.Name); - - // Handle dotted names for nested types (e.g., "Outer.Inner") - if (typeSymbol == null && annotation.Name.Contains('.', StringComparison.Ordinal)) - { - var parts = annotation.Name.Split('.'); - var outerSymbol = _symbolTable.LookupType(parts[0]); - if (outerSymbol != null) - { - for (int i = 1; i < parts.Length && outerSymbol != null; i++) - { - typeSymbol = outerSymbol.NestedTypes.FirstOrDefault( - n => n.Name == parts[i]); - outerSymbol = typeSymbol; - } - } - } + var typeSymbol = _symbolTable.LookupType(annotation.Name) + ?? LookupNestedType(annotation.Name); if (typeSymbol != null) { @@ -262,6 +247,25 @@ public SemanticType ResolveTypeAnnotation(TypeAnnotation? annotation) return result; } + private TypeSymbol? LookupNestedType(string dottedName) + { + if (!dottedName.Contains('.', StringComparison.Ordinal)) + return null; + + var parts = dottedName.Split('.'); + var outerSymbol = _symbolTable.LookupType(parts[0]); + if (outerSymbol == null) + return null; + + for (int i = 1; i < parts.Length && outerSymbol != null; i++) + { + var nested = outerSymbol.NestedTypes.FirstOrDefault(n => n.Name == parts[i]); + outerSymbol = nested; + } + + return outerSymbol; + } + private bool TryResolveBuiltinType(string name, out SemanticType type) { type = name switch @@ -360,7 +364,8 @@ private SemanticType ResolveGenericType(TypeAnnotation annotation) }; } - var typeSymbol = _symbolTable.LookupType(annotation.Name); + var typeSymbol = _symbolTable.LookupType(annotation.Name) + ?? LookupNestedType(annotation.Name); if (typeSymbol == null) { var genericMessage = $"Generic type '{annotation.Name}' not found"; From 4e6e60c51cc0c19a97f6e7094f9eacb0b7ceffa9 Mon Sep 17 00:00:00 2001 From: antonsynd Date: Thu, 23 Apr 2026 22:54:14 -0400 Subject: [PATCH 3/4] fix: allow nested types to access enclosing type's private/protected 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) --- .../nested_access_enclosing_private.expected | 1 + .../nested_access_enclosing_private.spy | 15 +++++++++++ ...nested_access_enclosing_protected.expected | 1 + .../nested_access_enclosing_protected.spy | 15 +++++++++++ .../Semantic/Validation/AccessValidator.cs | 27 ++++++++++++++----- 5 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_private.expected create mode 100644 src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_private.spy create mode 100644 src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_protected.expected create mode 100644 src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_protected.spy diff --git a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_private.expected b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_private.expected new file mode 100644 index 000000000..d81cc0710 --- /dev/null +++ b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_private.expected @@ -0,0 +1 @@ +42 diff --git a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_private.spy b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_private.spy new file mode 100644 index 000000000..981812f95 --- /dev/null +++ b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_private.spy @@ -0,0 +1,15 @@ +class Outer: + __secret: int + + def __init__(self, val: int): + self.__secret = val + + @public + class Inner: + def reveal(self, outer: Outer) -> int: + return outer.__secret + +def main(): + o: Outer = Outer(42) + i: Outer.Inner = Outer.Inner() + print(i.reveal(o)) diff --git a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_protected.expected b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_protected.expected new file mode 100644 index 000000000..3ad5abd03 --- /dev/null +++ b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_protected.expected @@ -0,0 +1 @@ +99 diff --git a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_protected.spy b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_protected.spy new file mode 100644 index 000000000..80374a0ef --- /dev/null +++ b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/nested_types/nested_access_enclosing_protected.spy @@ -0,0 +1,15 @@ +class Base: + _value: int + + def __init__(self, val: int): + self._value = val + + @public + class Helper: + def get_value(self, b: Base) -> int: + return b._value + +def main(): + b: Base = Base(99) + h: Base.Helper = Base.Helper() + print(h.get_value(b)) diff --git a/src/Sharpy.Compiler/Semantic/Validation/AccessValidator.cs b/src/Sharpy.Compiler/Semantic/Validation/AccessValidator.cs index 0696e1ee7..ef7bdcf4f 100644 --- a/src/Sharpy.Compiler/Semantic/Validation/AccessValidator.cs +++ b/src/Sharpy.Compiler/Semantic/Validation/AccessValidator.cs @@ -32,7 +32,8 @@ public override void Validate(Module module, SemanticContext context) public override void VisitClassDef(ClassDef node) { - var classSymbol = Context.SymbolTable.LookupType(node.Name); + var classSymbol = Context.SymbolTable.LookupType(node.Name) + ?? Context.Traversal.CurrentClass?.NestedTypes.FirstOrDefault(n => n.Name == node.Name); using (Context.Traversal.EnterClass(classSymbol)) { base.VisitClassDef(node); @@ -41,7 +42,8 @@ public override void VisitClassDef(ClassDef node) public override void VisitStructDef(StructDef node) { - var structSymbol = Context.SymbolTable.LookupType(node.Name); + var structSymbol = Context.SymbolTable.LookupType(node.Name) + ?? Context.Traversal.CurrentClass?.NestedTypes.FirstOrDefault(n => n.Name == node.Name); using (Context.Traversal.EnterClass(structSymbol)) { base.VisitStructDef(node); @@ -85,8 +87,8 @@ private void ValidateMemberAccess(string memberName, TypeSymbol owningType, int? switch (accessLevel) { case AccessLevel.Private: - // Private members only accessible within the same class - if (Context.Traversal.CurrentClass != owningType) + if (Context.Traversal.CurrentClass != owningType && + !IsNestedWithin(Context.Traversal.CurrentClass, owningType)) { AddError( $"Cannot access private member '{memberName}' of '{owningType.Name}' from outside the class", @@ -96,8 +98,9 @@ private void ValidateMemberAccess(string memberName, TypeSymbol owningType, int? break; case AccessLevel.Protected: - // Protected members accessible within the class hierarchy - if (Context.Traversal.CurrentClass == null || !IsInHierarchy(Context.Traversal.CurrentClass, owningType)) + if (Context.Traversal.CurrentClass == null || + (!IsInHierarchy(Context.Traversal.CurrentClass, owningType) && + !IsNestedWithin(Context.Traversal.CurrentClass, owningType))) { AddError( $"Cannot access protected member '{memberName}' of '{owningType.Name}' from outside the class hierarchy", @@ -174,4 +177,16 @@ private bool IsInHierarchy(TypeSymbol currentClass, TypeSymbol targetClass) return false; } + + 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; + } } From 345ee12e77d046f06169678f4709a812dfc20386 Mon Sep 17 00:00:00 2001 From: antonsynd Date: Thu, 23 Apr 2026 23:05:32 -0400 Subject: [PATCH 4/4] feat: serialize/deserialize nested types in SymbolSerializer (#591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../errors/dataclass_on_enum.error | 2 +- .../Project/IncrementalCompilationTests.cs | 58 ++++++++++++ .../Project/IncrementalCompilationCache.cs | 2 +- src/Sharpy.Compiler/Project/SymbolCache.cs | 5 + .../Project/SymbolSerializer.cs | 91 +++++++++++++------ .../Semantic/Validation/DecoratorValidator.cs | 7 ++ 6 files changed, 136 insertions(+), 29 deletions(-) diff --git a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/errors/dataclass_on_enum.error b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/errors/dataclass_on_enum.error index e2236d78e..04ce1ac25 100644 --- a/src/Sharpy.Compiler.Tests/Integration/TestFixtures/errors/dataclass_on_enum.error +++ b/src/Sharpy.Compiler.Tests/Integration/TestFixtures/errors/dataclass_on_enum.error @@ -1 +1 @@ -Decorators can only be applied to functions, classes, structs, interfaces, properties, events, or field declarations \ No newline at end of file +The '@dataclass' decorator can only be applied to classes, not to enum 'Color'. diff --git a/src/Sharpy.Compiler.Tests/Project/IncrementalCompilationTests.cs b/src/Sharpy.Compiler.Tests/Project/IncrementalCompilationTests.cs index 85b2fb8a7..c92320f0a 100644 --- a/src/Sharpy.Compiler.Tests/Project/IncrementalCompilationTests.cs +++ b/src/Sharpy.Compiler.Tests/Project/IncrementalCompilationTests.cs @@ -2014,4 +2014,62 @@ def some_function() -> int: } #endregion + + #region Nested Type Serialization + + [Fact] + public void SymbolSerializer_RoundTrip_NestedTypes() + { + var innerType = new TypeSymbol + { + Name = "Inner", + Kind = SymbolKind.Type, + TypeKind = TypeKind.Class, + AccessLevel = AccessLevel.Private, + DefiningFilePath = "/test/nested.spy", + DeclaringFilePath = "/test/nested.spy", + DeclarationLine = 2, + DeclarationColumn = 5, + Fields = new List + { + new VariableSymbol { Name = "value", Kind = SymbolKind.Variable, Type = BuiltinType.Int } + } + }; + + var outerType = new TypeSymbol + { + Name = "Outer", + Kind = SymbolKind.Type, + TypeKind = TypeKind.Class, + AccessLevel = AccessLevel.Public, + DefiningFilePath = "/test/nested.spy", + DeclaringFilePath = "/test/nested.spy", + DeclarationLine = 1, + DeclarationColumn = 1, + NestedTypes = new List { innerType } + }; + innerType.DeclaringType = outerType; + + var filePath = CreateTempFile("nested.spy", "class Outer:\n class Inner:\n value: int"); + var cached = SymbolSerializer.Serialize(outerType, filePath); + + Assert.NotNull(cached.NestedTypes); + Assert.Single(cached.NestedTypes!); + Assert.Equal("Inner", cached.NestedTypes[0].Name); + Assert.Equal("Type", cached.NestedTypes[0].Kind); + + var registry = new Dictionary(); + var restored = SymbolSerializer.Deserialize(cached, registry) as TypeSymbol; + + Assert.NotNull(restored); + Assert.Single(restored!.NestedTypes); + Assert.Equal("Inner", restored.NestedTypes[0].Name); + Assert.Equal(TypeKind.Class, restored.NestedTypes[0].TypeKind); + Assert.Equal(AccessLevel.Private, restored.NestedTypes[0].AccessLevel); + Assert.Equal(restored, restored.NestedTypes[0].DeclaringType); + Assert.Single(restored.NestedTypes[0].Fields); + Assert.Equal("value", restored.NestedTypes[0].Fields[0].Name); + } + + #endregion } diff --git a/src/Sharpy.Compiler/Project/IncrementalCompilationCache.cs b/src/Sharpy.Compiler/Project/IncrementalCompilationCache.cs index 4ec4b308f..6d3f8f96b 100644 --- a/src/Sharpy.Compiler/Project/IncrementalCompilationCache.cs +++ b/src/Sharpy.Compiler/Project/IncrementalCompilationCache.cs @@ -41,7 +41,7 @@ internal class IncrementalCompilationCache /// Current schema version for the symbol cache. /// Increment this when making breaking changes to FileCacheEntry or CachedSymbol structures. /// - internal const int CurrentSchemaVersion = 10; + internal const int CurrentSchemaVersion = 11; private readonly string _cacheFilePath; private readonly string _symbolCachePath; diff --git a/src/Sharpy.Compiler/Project/SymbolCache.cs b/src/Sharpy.Compiler/Project/SymbolCache.cs index 01c7e8d15..9293389d2 100644 --- a/src/Sharpy.Compiler/Project/SymbolCache.cs +++ b/src/Sharpy.Compiler/Project/SymbolCache.cs @@ -96,6 +96,11 @@ internal record CachedSymbol /// public List? Constructors { get; init; } + /// + /// For TypeSymbol: nested types (serialized as CachedSymbol with Kind=Type) + /// + public List? NestedTypes { get; init; } + /// /// For FunctionSymbol: parameters /// diff --git a/src/Sharpy.Compiler/Project/SymbolSerializer.cs b/src/Sharpy.Compiler/Project/SymbolSerializer.cs index 2bed5feac..98e527011 100644 --- a/src/Sharpy.Compiler/Project/SymbolSerializer.cs +++ b/src/Sharpy.Compiler/Project/SymbolSerializer.cs @@ -94,6 +94,14 @@ private static CachedSymbol SerializeTypeSymbol(TypeSymbol ts, string id, string SerializeFunctionSymbol(c, ComputeSymbolId(c, filePath), filePath)).ToList(); } + // Serialize nested types as nested CachedSymbols + List? nestedTypes = null; + if (ts.NestedTypes.Count > 0) + { + nestedTypes = ts.NestedTypes.Select(nt => + SerializeTypeSymbol(nt, ComputeSymbolId(nt, nt.DefiningFilePath ?? filePath), nt.DefiningFilePath ?? filePath)).ToList(); + } + return new CachedSymbol { Id = id, @@ -121,6 +129,7 @@ private static CachedSymbol SerializeTypeSymbol(TypeSymbol ts, string id, string Fields = fields, Methods = methods, Constructors = constructors, + NestedTypes = nestedTypes, IsReExport = ts.IsReExport, OriginalModule = ts.OriginalModule, CodeGenInfo = SerializeCodeGenInfo(ts.CodeGenInfo), @@ -322,6 +331,11 @@ private static TypeSymbol DeserializeTypeSymbol( var constructors = cached.Constructors?.Select(c => DeserializeFunctionSymbol(c, typeResolver)).ToList() ?? new List(); + // Deserialize nested types + var nestedTypes = cached.NestedTypes? + .Select(nt => DeserializeTypeSymbol(nt, symbolRegistry, typeResolver)) + .ToList() ?? new List(); + var symbol = new TypeSymbol { Name = cached.Name, @@ -338,6 +352,7 @@ private static TypeSymbol DeserializeTypeSymbol( Fields = fields, Methods = methods, Constructors = constructors, + NestedTypes = nestedTypes, IsReExport = cached.IsReExport, OriginalModule = cached.OriginalModule, CodeGenInfo = DeserializeCodeGenInfo(cached.CodeGenInfo) @@ -345,6 +360,13 @@ private static TypeSymbol DeserializeTypeSymbol( symbol.Documentation = cached.Documentation; + // Set DeclaringType on nested types and register them + foreach (var nested in nestedTypes) + { + nested.DeclaringType = symbol; + symbolRegistry[ComputeSymbolId(nested, nested.DefiningFilePath ?? cached.FilePath)] = nested; + } + // BaseType and Interfaces resolved in a second pass via symbolRegistry return symbol; } @@ -735,33 +757,7 @@ public static void ResolveReferences( // Resolve TypeSymbol references if (symbol is TypeSymbol ts) { - // Resolve BaseType - if (cached.BaseTypeId != null && symbolRegistry.TryGetValue(cached.BaseTypeId, out var baseSymbol)) - { - if (baseSymbol is TypeSymbol baseType) - { - ts.BaseType = baseType; - } - } - - // Resolve Interfaces - if (cached.InterfaceEntries != null) - { - foreach (var entry in cached.InterfaceEntries) - { - if (symbolRegistry.TryGetValue(entry.SymbolId, out var ifaceSymbol) && ifaceSymbol is TypeSymbol ifaceType) - { - var typeArgs = entry.TypeArgs != null && entry.TypeArgs.Count > 0 - ? entry.TypeArgs.Select(DeserializeTypeAnnotation).ToImmutableArray() - : ImmutableArray.Empty; - ts.Interfaces.Add(new InterfaceReference - { - Definition = ifaceType, - TypeArgAnnotations = typeArgs - }); - } - } - } + ResolveTypeReferences(cached, ts, symbolRegistry); } // Resolve ModuleSymbol exports @@ -778,6 +774,47 @@ public static void ResolveReferences( } } + private static void ResolveTypeReferences( + CachedSymbol cached, TypeSymbol ts, Dictionary symbolRegistry) + { + // Resolve BaseType + if (cached.BaseTypeId != null && symbolRegistry.TryGetValue(cached.BaseTypeId, out var baseSymbol)) + { + if (baseSymbol is TypeSymbol baseType) + { + ts.BaseType = baseType; + } + } + + // Resolve Interfaces + if (cached.InterfaceEntries != null) + { + foreach (var entry in cached.InterfaceEntries) + { + if (symbolRegistry.TryGetValue(entry.SymbolId, out var ifaceSymbol) && ifaceSymbol is TypeSymbol ifaceType) + { + var typeArgs = entry.TypeArgs != null && entry.TypeArgs.Count > 0 + ? entry.TypeArgs.Select(DeserializeTypeAnnotation).ToImmutableArray() + : ImmutableArray.Empty; + ts.Interfaces.Add(new InterfaceReference + { + Definition = ifaceType, + TypeArgAnnotations = typeArgs + }); + } + } + } + + // Recursively resolve nested types' references + if (cached.NestedTypes != null) + { + for (int i = 0; i < cached.NestedTypes.Count && i < ts.NestedTypes.Count; i++) + { + ResolveTypeReferences(cached.NestedTypes[i], ts.NestedTypes[i], symbolRegistry); + } + } + } + #endregion /// diff --git a/src/Sharpy.Compiler/Semantic/Validation/DecoratorValidator.cs b/src/Sharpy.Compiler/Semantic/Validation/DecoratorValidator.cs index 167a81c84..0a0fffe0b 100644 --- a/src/Sharpy.Compiler/Semantic/Validation/DecoratorValidator.cs +++ b/src/Sharpy.Compiler/Semantic/Validation/DecoratorValidator.cs @@ -110,6 +110,13 @@ public override void VisitInterfaceDef(InterfaceDef node) _containingType = previousType; } + public override void VisitEnumDef(EnumDef node) + { + ValidateDecorators(node.Decorators, node.Name); + ValidateDataclassOnNonClass(node.Decorators, node.Name, "enum"); + base.VisitEnumDef(node); + } + public override void VisitPropertyDef(PropertyDef node) { var definitionName = _containingType != null