From 18295420b65cd39b13886090a80e34f6a7779ecd Mon Sep 17 00:00:00 2001 From: R-unic Date: Mon, 29 Jul 2024 23:52:14 -0400 Subject: [PATCH] feat: include System.Collections & System.Core core libraries --- .gitignore | 3 +- RobloxCS/CodeGenerator.cs | 39 +++++----- RobloxCS/README.md | 7 +- RobloxCS/RobloxCS.csproj | 2 +- RobloxCS/Transformers/MainTransformer.cs | 15 ++-- RobloxCS/TranspilerUtility.cs | 17 ++--- RobloxCS/Utility.cs | 7 +- TestProject/TestProject.csproj | 14 ---- TestProject/aftman.toml | 7 -- TestProject/default.project.json | 48 ------------- TestProject/dist/Client/Components/Lava.lua | 40 ----------- TestProject/dist/Client/Main.client.lua | 21 ------ TestProject/dist/Shared/Components.lua | 76 -------------------- TestProject/dist/server/BasicLava.server.lua | 11 --- TestProject/roblox-cs.yml | 7 -- TestProject/src/Client/Components/Lava.cs | 33 --------- TestProject/src/Client/Main.client.cs | 12 ---- TestProject/src/Server/BasicLava.server.cs | 14 ---- TestProject/src/Shared/Components.cs | 70 ------------------ 19 files changed, 48 insertions(+), 395 deletions(-) delete mode 100644 TestProject/TestProject.csproj delete mode 100644 TestProject/aftman.toml delete mode 100644 TestProject/default.project.json delete mode 100644 TestProject/dist/Client/Components/Lava.lua delete mode 100644 TestProject/dist/Client/Main.client.lua delete mode 100644 TestProject/dist/Shared/Components.lua delete mode 100644 TestProject/dist/server/BasicLava.server.lua delete mode 100644 TestProject/roblox-cs.yml delete mode 100644 TestProject/src/Client/Components/Lava.cs delete mode 100644 TestProject/src/Client/Main.client.cs delete mode 100644 TestProject/src/Server/BasicLava.server.cs delete mode 100644 TestProject/src/Shared/Components.cs diff --git a/.gitignore b/.gitignore index 50715e3..b7aeedc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ bin/ obj/ .vs/ -include/ \ No newline at end of file +include/ +TestProject/ \ No newline at end of file diff --git a/RobloxCS/CodeGenerator.cs b/RobloxCS/CodeGenerator.cs index 3629569..8cec484 100644 --- a/RobloxCS/CodeGenerator.cs +++ b/RobloxCS/CodeGenerator.cs @@ -978,7 +978,7 @@ private void FullyQualifyMemberAccess(INamespaceSymbol? namespaceType, List usingDirective.Name != null && Utility.GetNamesFromNode(usingDirective).Contains(namespaceType.ContainingNamespace.Name)); + var typeIsImported = usings.Any(usingDirective => namespaceType.ContainingNamespace != null && usingDirective.Name != null && Utility.GetNamesFromNode(usingDirective).Contains(namespaceType.ContainingNamespace.Name)); Write($"CS.getAssemblyType(\"{namespaceType.Name}\")."); _flags[CodeGenFlag.ShouldCallGetAssemblyType] = false; } @@ -1576,7 +1576,7 @@ private void WriteName(SyntaxNode node, SyntaxToken identifier) || (pluginClassesNamespace != null && IsDescendantOfNamespaceSymbol(symbol, pluginClassesNamespace)) ) : false; - List fullyQualifiedParentKinds = [SyntaxKind.SimpleMemberAccessExpression, SyntaxKind.ObjectCreationExpression]; + HashSet fullyQualifiedParentKinds = [SyntaxKind.SimpleMemberAccessExpression, SyntaxKind.ObjectCreationExpression]; if ( symbol != null && symbol is ITypeSymbol typeSymbol @@ -1594,24 +1594,23 @@ private void WriteName(SyntaxNode node, SyntaxToken identifier) var parentAccessExpression = FindFirstAncestor(node); var isLeftSide = parentAccessExpression == null ? true : node == parentAccessExpression.Expression; var parentBlocks = GetAncestors(node); - var localScopeIncludesIdentifier = parentBlocks - .Any(block => - { - var descendants = block.DescendantNodes(); - var localFunctions = descendants.OfType(); - var variableDesignations = descendants.OfType(); - var variableDeclarators = descendants.OfType(); - var forEachStatements = descendants.OfType(); - var forStatements = descendants.OfType(); - var parameters = descendants.OfType(); - var checkNamePredicate = (SyntaxNode node) => TryGetName(node) == identifierText; - return localFunctions.Any(checkNamePredicate) - || variableDesignations.Any(checkNamePredicate) - || variableDeclarators.Any(checkNamePredicate) - || parameters.Any(checkNamePredicate) - || forEachStatements.Any(checkNamePredicate) - || forStatements.Any(forStatement => forStatement.Initializers.Count() > 0); - }); + var localScopeIncludesIdentifier = parentBlocks.Any(block => + { + var descendants = block.DescendantNodes(); + var localFunctions = descendants.OfType(); + var variableDesignations = descendants.OfType(); + var variableDeclarators = descendants.OfType(); + var forEachStatements = descendants.OfType(); + var forStatements = descendants.OfType(); + var parameters = descendants.OfType(); + var checkNamePredicate = (SyntaxNode node) => TryGetName(node) == identifierText; + return localFunctions.Any(checkNamePredicate) + || variableDesignations.Any(checkNamePredicate) + || variableDeclarators.Any(checkNamePredicate) + || parameters.Any(checkNamePredicate) + || forEachStatements.Any(checkNamePredicate) + || forStatements.Any(forStatement => forStatement.Initializers.Count() > 0); + }); if (isLeftSide && !localScopeIncludesIdentifier && !runtimeNamespaceIncludesIdentifier) { diff --git a/RobloxCS/README.md b/RobloxCS/README.md index af0afd5..39de847 100644 --- a/RobloxCS/README.md +++ b/RobloxCS/README.md @@ -14,6 +14,10 @@ This project includes the compiler and transformers. - Interfaces to Luau `export type` - Anonymous types (e.g. `var x = new { };`) - Type hoisting when outside of namespace +- Macro `Equals` function to `__eq` +- Macro `GetType` function to the name of the type as a string +- Async/await +- Overloaded methods ## Will maybe be supported - [Class finalizers (destructors)](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/finalizers) @@ -25,5 +29,4 @@ This project includes the compiler and transformers. ## Will not be supported - `using name = value` expressions (equivalent to `:=` operator in other languages) -- `volatile`, `fixed`, and `unsafe` keywords -- Pointers \ No newline at end of file +- Any unsafe context (pointers, `volatile`, `fixed`, etc.) \ No newline at end of file diff --git a/RobloxCS/RobloxCS.csproj b/RobloxCS/RobloxCS.csproj index b7ce1c1..37c4a8f 100644 --- a/RobloxCS/RobloxCS.csproj +++ b/RobloxCS/RobloxCS.csproj @@ -12,7 +12,7 @@ - + diff --git a/RobloxCS/Transformers/MainTransformer.cs b/RobloxCS/Transformers/MainTransformer.cs index c6ee604..60264ae 100644 --- a/RobloxCS/Transformers/MainTransformer.cs +++ b/RobloxCS/Transformers/MainTransformer.cs @@ -1,9 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.IO; -using System.Linq.Expressions; -using System.Text.RegularExpressions; namespace RobloxCS { @@ -47,12 +44,6 @@ public MainTransformer(SyntaxTree tree, ConfigData config) return base.VisitIdentifierName(node.WithIdentifier(newToken)); } - private static SyntaxToken CreateIdentifierToken(string text, string? valueText = null, SyntaxTriviaList? trivia = null) - { - var triviaList = trivia ??= SyntaxFactory.TriviaList(); - return SyntaxFactory.VerbatimIdentifier(triviaList, text, valueText ?? text, triviaList); - } - public override SyntaxNode? VisitArgument(ArgumentSyntax node) { if (node.Expression.IsKind(SyntaxKind.IdentifierName)) @@ -73,6 +64,12 @@ private static SyntaxToken CreateIdentifierToken(string text, string? valueText return base.VisitConditionalAccessExpression(node); } + private static SyntaxToken CreateIdentifierToken(string text, string? valueText = null, SyntaxTriviaList? trivia = null) + { + var triviaList = trivia ??= SyntaxFactory.TriviaList(); + return SyntaxFactory.VerbatimIdentifier(triviaList, text, valueText ?? text, triviaList); + } + private ExpressionSyntax? ProcessWhenNotNull(ExpressionSyntax expression, ExpressionSyntax whenNotNull) { if (whenNotNull == null) diff --git a/RobloxCS/TranspilerUtility.cs b/RobloxCS/TranspilerUtility.cs index b82dc2d..928f61b 100644 --- a/RobloxCS/TranspilerUtility.cs +++ b/RobloxCS/TranspilerUtility.cs @@ -102,17 +102,18 @@ private static List GetCompilationReferences() return references; } - private static List GetCoreLibReferences() + private static HashSet GetCoreLibReferences() { var coreLib = typeof(object).GetTypeInfo().Assembly.Location; - var systemRuntime = Path.Combine(Path.GetDirectoryName(coreLib)!, "System.Runtime.dll"); - var systemConsole = Path.Combine(Path.GetDirectoryName(coreLib)!, "System.Console.dll"); - return new List + HashSet coreDlls = ["System.Runtime.dll", "System.Core.dll", "System.Console.dll", "System.Collections.dll"]; + HashSet references = [MetadataReference.CreateFromFile(coreLib)]; + + foreach (var coreDll in coreDlls) { - MetadataReference.CreateFromFile(coreLib), - MetadataReference.CreateFromFile(systemRuntime), - MetadataReference.CreateFromFile(systemConsole) - }; + var dllPath = Path.Combine(Path.GetDirectoryName(coreLib)!, coreDll); + references.Add(MetadataReference.CreateFromFile(dllPath)); + } + return references; } } } diff --git a/RobloxCS/Utility.cs b/RobloxCS/Utility.cs index 3486505..89837fe 100644 --- a/RobloxCS/Utility.cs +++ b/RobloxCS/Utility.cs @@ -130,7 +130,12 @@ public static string FormatLocation(FileLinePositionSpan lineSpan) public static ISymbol? FindMember(INamespaceSymbol namespaceSymbol, string memberName) { - return namespaceSymbol.GetMembers().FirstOrDefault(member => member.Name == memberName); + var member = namespaceSymbol.GetMembers().FirstOrDefault(member => member?.Name == memberName, null); + if (member == null && namespaceSymbol.ContainingNamespace != null) + { + member = FindMember(namespaceSymbol.ContainingNamespace, memberName); + } + return member; } public static ISymbol? FindMemberDeep(INamedTypeSymbol namedTypeSymbol, string memberName) diff --git a/TestProject/TestProject.csproj b/TestProject/TestProject.csproj deleted file mode 100644 index 219d620..0000000 --- a/TestProject/TestProject.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - - - - - diff --git a/TestProject/aftman.toml b/TestProject/aftman.toml deleted file mode 100644 index e989d3b..0000000 --- a/TestProject/aftman.toml +++ /dev/null @@ -1,7 +0,0 @@ -# This file lists tools managed by Aftman, a cross-platform toolchain manager. -# For more information, see https://github.com/LPGhatguy/aftman - -# To add a new tool, add an entry to this table. -[tools] -rojo = "rojo-rbx/rojo@7.4.1" -# rojo = "rojo-rbx/rojo@6.2.0" \ No newline at end of file diff --git a/TestProject/default.project.json b/TestProject/default.project.json deleted file mode 100644 index f38f547..0000000 --- a/TestProject/default.project.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "roblox-cs Test Project", - "tree": { - "$className": "DataModel", - "ServerScriptService": { - "$className": "ServerScriptService", - "C#": { - "$path": "dist/Server" - } - }, - "ReplicatedStorage": { - "$className": "ReplicatedStorage", - "rbxcs_include": { - "$path": "./include" - }, - "C#": { - "$path": "dist/Shared" - } - }, - "StarterPlayer": { - "$className": "StarterPlayer", - "StarterPlayerScripts": { - "$className": "StarterPlayerScripts", - "C#": { - "$path": "dist/Client" - } - } - }, - "Workspace": { - "$className": "Workspace", - "$properties": { - "FilteringEnabled": true - } - }, - "HttpService": { - "$className": "HttpService", - "$properties": { - "HttpEnabled": true - } - }, - "SoundService": { - "$className": "SoundService", - "$properties": { - "RespectFilteringEnabled": true - } - } - } -} \ No newline at end of file diff --git a/TestProject/dist/Client/Components/Lava.lua b/TestProject/dist/Client/Components/Lava.lua deleted file mode 100644 index c0d0170..0000000 --- a/TestProject/dist/Client/Components/Lava.lua +++ /dev/null @@ -1,40 +0,0 @@ -local CS = require(game:GetService("ReplicatedStorage")["rbxcs_include"]["RuntimeLib"]) - -CS.namespace("TestGame", @native function(namespace: CS.Namespace) - namespace:namespace("Client", @native function(namespace: CS.Namespace) - namespace:class("LavaComponent", @native function(namespace: CS.Namespace) - local class = CS.classDef("LavaComponent", namespace, "Components.GameComponent") - - function class.new(instance: Part) - local mt = {} - local self = CS.classInstance(class, mt, namespace) - - self["$base"](instance) - - function self.Start(): nil - self.Instance.Touched:Connect(function(hit) - local model = hit:FindFirstAncestorOfClass("Model") - local humanoid = if model == nil then nil else model:FindFirstChildOfClass("Humanoid") - if humanoid == nil then - return - end - humanoid:TakeDamage(10) - end) - return nil :: any - end - function self.Update(dt: number): nil - return nil :: any - end - function self.Destroy(): nil - return nil :: any - end - - return self - end - - return class - end) - end) -end) - -return {} \ No newline at end of file diff --git a/TestProject/dist/Client/Main.client.lua b/TestProject/dist/Client/Main.client.lua deleted file mode 100644 index a2afed6..0000000 --- a/TestProject/dist/Client/Main.client.lua +++ /dev/null @@ -1,21 +0,0 @@ -local CS = require(game:GetService("ReplicatedStorage")["rbxcs_include"]["RuntimeLib"]) - -CS.namespace("TestGame", @native function(namespace: CS.Namespace) - namespace:namespace("Client", @native function(namespace: CS.Namespace) - namespace:class("Game", @native function(namespace: CS.Namespace) - local class = CS.classDef("Game", namespace) - - function class.Main(): nil - print("[TestProject/Client/Main.client.cs:9:13]:", "rah") - return nil :: any - end - - if namespace == nil then - class.Main() - else - namespace["$onLoaded"](namespace, class.Main) - end - return class - end) - end) -end) \ No newline at end of file diff --git a/TestProject/dist/Shared/Components.lua b/TestProject/dist/Shared/Components.lua deleted file mode 100644 index a793f78..0000000 --- a/TestProject/dist/Shared/Components.lua +++ /dev/null @@ -1,76 +0,0 @@ -local CS = require(game:GetService("ReplicatedStorage")["rbxcs_include"]["RuntimeLib"]) - -CS.namespace("Components", @native function(namespace: CS.Namespace) - namespace:class("ComponentRunner", @native function(namespace: CS.Namespace) - local class = CS.classDef("ComponentRunner", namespace) - - function class.AttachTag(tag: string, attachComponent: Func): nil - local attached = false - local instances = game:GetService("CollectionService"):GetTagged(tag) - game:GetService("CollectionService").TagAdded:Connect(function(tag) - if attached then - return - end - local instance = game:GetService("CollectionService"):GetTagged(tag)[1] - class.Run(attachComponent(instance)) - attached = true - end) - for _, instance in instances do - if attached then - continueend - class.Run(attachComponent(instance)) - attached = true - end - return nil :: any - end - function class.Run(component: GameComponent): nil - component:Start() - local updateEvent = class.GetUpdateEvent(component) - updateEvent:Connect(component.Update) - return nil :: any - end - function class.GetUpdateEvent(component: GameComponent): ScriptSignal - if component.UpdateMethod == "RenderStepped" then - return game:GetService("RunService").RenderStepped - elseif component.UpdateMethod == "Heartbeat" then - return game:GetService("RunService").Heartbeat - else - return game:GetService("RunService").Heartbeat - end - return nil :: any - end - - return class - end) - namespace:class("GameComponent", @native function(namespace: CS.Namespace) - local class = CS.classDef("GameComponent", namespace) - - function class.new() - local mt = {} - local self = CS.classInstance(class, mt, namespace) - - self.UpdateMethod = if game:GetService("RunService"):IsClient() then "RenderStepped" else "Heartbeat" - - return self - end - - return class - end) - namespace:class("GameComponent", @native function(namespace: CS.Namespace) - local class = CS.classDef("GameComponent", namespace, "Components.GameComponent") - - function class.new(instance: TInstance) - local mt = {} - local self = CS.classInstance(class, mt, namespace) - - - self.Instance = instance - - return self - end - - return class - end) -end) - -return {} \ No newline at end of file diff --git a/TestProject/dist/server/BasicLava.server.lua b/TestProject/dist/server/BasicLava.server.lua deleted file mode 100644 index 79b2c0d..0000000 --- a/TestProject/dist/server/BasicLava.server.lua +++ /dev/null @@ -1,11 +0,0 @@ -local CS = require(game:GetService("ReplicatedStorage")["rbxcs_include"]["RuntimeLib"]) - -for _, instance in game:GetService("CollectionService"):GetTagged("Lava") do - print("[TestProject/Server/BasicLava.server.cs:5:5]:", not CS.is(instance, BasePart)) - if CS.is(instance, "BasePart") then - local part = instance - part.Touched:Connect(function(part) - return if part.Parent == nil then nil else if part.Parent:FindFirstChildOfClass("Humanoid") == nil then nil else part.Parent:FindFirstChildOfClass("Humanoid"):TakeDamage(100) - end) - end -end \ No newline at end of file diff --git a/TestProject/roblox-cs.yml b/TestProject/roblox-cs.yml deleted file mode 100644 index 87d4c67..0000000 --- a/TestProject/roblox-cs.yml +++ /dev/null @@ -1,7 +0,0 @@ -SourceFolder: src -OutputFolder: dist -CSharpOptions: - EntryPointRequired: false - EntryPointName: Game - MainMethodName: Main - AssemblyName: RobloxCSProject \ No newline at end of file diff --git a/TestProject/src/Client/Components/Lava.cs b/TestProject/src/Client/Components/Lava.cs deleted file mode 100644 index 4884155..0000000 --- a/TestProject/src/Client/Components/Lava.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Roblox; -using Components; - -namespace TestGame.Client -{ - public class LavaComponent : GameComponent - { - public LavaComponent(Part instance) - : base(instance) - { - } - - public override void Start() - { - Instance.Touched.Connect(hit => - { - var model = hit.FindFirstAncestorOfClass(); - var humanoid = model?.FindFirstChildOfClass(); - if (humanoid == null) return; - - humanoid.TakeDamage(10); - }); - } - - public override void Update(double dt) - { - } - - public override void Destroy() - { - } - } -} \ No newline at end of file diff --git a/TestProject/src/Client/Main.client.cs b/TestProject/src/Client/Main.client.cs deleted file mode 100644 index 791a93a..0000000 --- a/TestProject/src/Client/Main.client.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Roblox; - -namespace TestGame.Client -{ - public static class Game - { - public static void Main() - { - Console.WriteLine("rah"); - } - } -} \ No newline at end of file diff --git a/TestProject/src/Server/BasicLava.server.cs b/TestProject/src/Server/BasicLava.server.cs deleted file mode 100644 index 0c3a8be..0000000 --- a/TestProject/src/Server/BasicLava.server.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Roblox; - -foreach (var instance in Services.CollectionService.GetTagged("Lava")) -{ - Console.WriteLine(instance is not BasePart); - if (instance is BasePart part) - { - part.Touched.Connect(part => - part.Parent? - .FindFirstChildOfClass()? - .TakeDamage(100) - ); - } -} \ No newline at end of file diff --git a/TestProject/src/Shared/Components.cs b/TestProject/src/Shared/Components.cs deleted file mode 100644 index 8b3c9bf..0000000 --- a/TestProject/src/Shared/Components.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Roblox; - -namespace Components -{ - public static class ComponentRunner - { - public static void AttachTag(string tag, Func attachComponent) where TComponent : GameComponent - { - var attached = false; - var instances = Services.CollectionService.GetTagged(tag); - Services.CollectionService.TagAdded.Connect(tag => - { - if (attached) return; - var instance = Services.CollectionService.GetTagged(tag)[0]; - Run(attachComponent(instance)); - attached = true; - }); - - foreach (var instance in instances) - { - if (attached) continue; - Run(attachComponent(instance)); - attached = true; - } - } - - public static void Run(GameComponent component) - { - component.Start(); - - var updateEvent = GetUpdateEvent(component); - updateEvent.Connect(component.Update); - } - - private static ScriptSignal GetUpdateEvent(GameComponent component) - { - if (component.UpdateMethod == "RenderStepped") - { - return Services.RunService.RenderStepped; - } - else if (component.UpdateMethod == "Heartbeat") - { - return Services.RunService.Heartbeat; - } - else - { - return Services.RunService.Heartbeat; - } - } - } - - public abstract class GameComponent - { - public string UpdateMethod { get; set; } = Services.RunService.IsClient() ? "RenderStepped" : "Heartbeat"; - - public abstract void Start(); - public abstract void Update(double dt); - public abstract void Destroy(); - } - - public abstract class GameComponent : GameComponent where TInstance : Instance - { - public TInstance Instance { get; } - - protected GameComponent(TInstance instance) - { - Instance = instance; - } - } -} \ No newline at end of file