Skip to content

Commit

Permalink
Merge pull request #29 from roblox-csharp/feat/events
Browse files Browse the repository at this point in the history
Basic events implementation
  • Loading branch information
R-unic authored Jan 8, 2025
2 parents 155a587 + 1142686 commit 4cb66b3
Show file tree
Hide file tree
Showing 17 changed files with 542 additions and 89 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ jobs:
run: dotnet pack -c Release

- name: Run tests
run: dotnet test --verbosity normal
run: |
chmod +x ./RobloxCS.Tests/lune
dotnet test
- name: Check if version is already published
id: check_version
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:

- name: Run tests and generate coverage
run: |
chmod +x ./RobloxCS.Tests/lune
dotnet test -c Release --collect:"XPlat Code Coverage"
- name: Convert coverage to LCOV
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
.idea/
Properties/

RobloxCS.Tests/**/out
RobloxCS.Tests/**/include
RobloxCS.Tests/**/package-lock.json
*.DotSettings.user
test.cs

Expand Down
6 changes: 2 additions & 4 deletions RobloxCS.Luau/AST/NoOp.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
namespace RobloxCS.Luau
{
/// <summary>Simply renders nothing.</summary>
/// <summary>Simply renders a newline.</summary>
public sealed class NoOp : Statement
{
public override void Render(LuauWriter luau)
{
}
public override void Render(LuauWriter luau) => luau.WriteLine();
}
}
18 changes: 12 additions & 6 deletions RobloxCS.Luau/AstUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,16 @@ public static Call CSCall(string methodName, params Expression[] arguments) =>
CreateArgumentList(arguments.ToList())
);

public static Call PrintCall(params List<Luau.Expression> args) =>
public static Call RequireCall(Expression modulePath) =>
new(
new IdentifierName("require"),
new ArgumentList([new Argument(modulePath)])
);

public static Call PrintCall(params List<Expression> args) =>
new(
new IdentifierName("print"),
new ArgumentList(args.ConvertAll(value => new Luau.Argument(value)))
new ArgumentList(args.ConvertAll(value => new Argument(value)))
);

/// <summary>
Expand Down Expand Up @@ -328,16 +334,16 @@ public static IdentifierName GetNonGenericName(SimpleName simpleName)
: simpleName.ToString());
}

public static Name CreateName(string text)
public static Name CreateName(SyntaxNode node, string text, bool registerIdentifier = false, bool bypassReserved = false)
{
Name expression = new IdentifierName(text);
Name name = CreateSimpleName(node, text, registerIdentifier, bypassReserved);
var pieces = text.Split('.');
if (pieces.Length <= 0)
return expression;
return name;

return pieces
.Skip(1)
.Aggregate(expression, (current, piece) => new QualifiedName(current, new IdentifierName(piece)));
.Aggregate(name, (current, piece) => new QualifiedName(current, CreateSimpleName(node, piece)));
}

public static TNameNode CreateSimpleName<TNameNode>(SyntaxNode node, bool registerIdentifier = false, bool bypassReserved = false)
Expand Down
78 changes: 74 additions & 4 deletions RobloxCS.Luau/Macros.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,83 @@ public enum MacroKind
ObjectMethod,
IEnumerableMethod,
ListMethod,
DictionaryMethod
DictionaryMethod,
BitOperation
}

public class Macro(SemanticModel semanticModel)
{
private SemanticModel _semanticModel { get; } = semanticModel;

public Node? Assignment(Func<SyntaxNode, Node?> visit, AssignmentExpressionSyntax assignment)
{
var mappedOperator = StandardUtility.GetMappedOperator(assignment.OperatorToken.Text);
var bit32MethodName = StandardUtility.GetBit32MethodName(mappedOperator);
if (bit32MethodName != null)
{
var target = (AssignmentTarget)visit(assignment.Left)!;
var value = (Expression)visit(assignment.Right)!;
var bit32Call = AstUtility.Bit32Call(bit32MethodName, target, value);
bit32Call.MarkExpanded(MacroKind.BitOperation);

return new Assignment(target, AstUtility.Bit32Call(bit32MethodName, target, value));
}

var leftSymbol = _semanticModel.GetSymbolInfo(assignment.Left).Symbol;
if (leftSymbol is IEventSymbol eventSymbol)
{
var symbolMetadata = SymbolMetadataManager.Get(eventSymbol);
symbolMetadata.EventConnectionName ??= AstUtility.CreateSimpleName<IdentifierName>(assignment, "conn_" + eventSymbol.Name, registerIdentifier: true);

var connectionName = symbolMetadata.EventConnectionName;
switch (mappedOperator)
{
case "+=":
{
var left = (Expression)visit(assignment.Left)!;
var right = (Expression)visit(assignment.Right)!;
return new Variable(
connectionName,
true,
new Call(
new MemberAccess(left, new IdentifierName("Connect"), ':'),
new ArgumentList([new Argument(right)])
)
);
}
case "-=":
{
return new Call(
new MemberAccess(connectionName, new IdentifierName("Disconnect"), ':'),
new ArgumentList([])
);
}
}
}

return null;
}

public Expression? BinaryExpression(Func<SyntaxNode, Node?> visit, BinaryExpressionSyntax binaryExpression)
{
var mappedOperator = StandardUtility.GetMappedOperator(binaryExpression.OperatorToken.Text);
var bit32MethodName = StandardUtility.GetBit32MethodName(mappedOperator);
if (bit32MethodName != null)
{
var left = (Expression)visit(binaryExpression.Left)!;
var right = (Expression)visit(binaryExpression.Right)!;
var bit32Call = AstUtility.Bit32Call(bit32MethodName, left, right);
bit32Call.MarkExpanded(MacroKind.BitOperation);

return bit32Call;
}

return null;
}

/// <summary>
/// Takes a C# generic name and expands the name into a macro'd type
/// </summary>
public Name? GenericName(Func<SyntaxNode, Node?> visit, GenericNameSyntax genericName)
{
var typeInfo = _semanticModel.GetTypeInfo(genericName);
Expand Down Expand Up @@ -123,11 +193,11 @@ public class Macro(SemanticModel semanticModel)
/// <returns>The expanded expression of the macro, or null if no macro was applied</returns>
public Expression? ObjectCreation(Func<SyntaxNode, Node?> visit, BaseObjectCreationExpressionSyntax baseObjectCreation) {
// generic objects
var type = (INamedTypeSymbol)(baseObjectCreation is ObjectCreationExpressionSyntax objectCreation
var type = (baseObjectCreation is ObjectCreationExpressionSyntax objectCreation
? _semanticModel.GetSymbolInfo(objectCreation.Type)
: _semanticModel.GetSymbolInfo(baseObjectCreation)).Symbol!.ContainingSymbol;
: _semanticModel.GetSymbolInfo(baseObjectCreation)).Symbol!.ContainingSymbol as INamedTypeSymbol;

if (type.TypeParameters.Length > 0) {
if (type is { TypeParameters.Length: > 0 }) {
switch (type.Name) {
case "List":
{
Expand Down
22 changes: 22 additions & 0 deletions RobloxCS.Luau/SymbolMetadataManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.CodeAnalysis;

namespace RobloxCS;

public class SymbolMetadata
{
public Luau.IdentifierName? EventConnectionName { get; set; }
}

public static class SymbolMetadataManager
{
private static readonly Dictionary<ISymbol, SymbolMetadata> _metadata = [];

public static SymbolMetadata Get(ISymbol symbol)
{
var metadata = _metadata.GetValueOrDefault(symbol);
if (metadata == null)
_metadata.Add(symbol, metadata = new SymbolMetadata());

return metadata;
}
}
22 changes: 22 additions & 0 deletions RobloxCS.Tests/.lune/RuntimeLibTest.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
local CS = require("../../RobloxCS/Include/RuntimeLib")

-- CS.is()
assert(true, CS.is("abc", "string"))
assert(true, CS.is(123, "number"))
assert(true, CS.is(false, "boolean"))
assert(true, CS.is(nil, "nil"))
assert(true, CS.is(function() end, "function"))

local MyClass: CS.Class = { __className = "MyClass" }
local myClass = { __className = "MyClass" }
assert(true, CS.is(myClass, MyClass))

local parent = {}
parent.__index = parent
local child = setmetatable({}, parent)
assert(true, CS.is(child, parent))

-- CS.defineGlobal() / CS.getGlobal()
CS.defineGlobal("MyValue", 69420)
assert(69420, CS.getGlobal("MyValue"))

4 changes: 4 additions & 0 deletions RobloxCS.Tests/RobloxCS.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@
<ProjectReference Include="..\RobloxCS\RobloxCS.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include=".lune\RuntimeLibTest.luau" />
</ItemGroup>

</Project>
54 changes: 54 additions & 0 deletions RobloxCS.Tests/RuntimeLibTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using Xunit.Abstractions;

namespace RobloxCS.Tests;

public class RuntimeLibTest(ITestOutputHelper testOutputHelper)
{
private readonly string _cwd =
Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))))!;

[Theory]
[InlineData("RuntimeLibTest")]
public void RuntimeLib_PassesTests(string scriptName)
{
var lunePath = Path.GetFullPath("lune" + (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : ""), _cwd);
var runScriptArguments = $"run {scriptName}";
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = lunePath,
Arguments = runScriptArguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = _cwd
}
};

try
{
process.Start();

var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit();

testOutputHelper.WriteLine($"{scriptName}.luau Errors:");
testOutputHelper.WriteLine(error);
Assert.True(string.IsNullOrWhiteSpace(error));
testOutputHelper.WriteLine($"{scriptName}.luau Output:");
testOutputHelper.WriteLine(output);
Assert.True(string.IsNullOrWhiteSpace(output));
Assert.Equal(0, process.ExitCode);
}
finally
{
process.Dispose();
}
}
}
Binary file added RobloxCS.Tests/lune
Binary file not shown.
Binary file added RobloxCS.Tests/lune.exe
Binary file not shown.
2 changes: 1 addition & 1 deletion RobloxCS.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RobloxCS.Luau", "RobloxCS.L
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{44CF4B5D-C4EB-41BF-8D70-20D5213AFA4A}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
.github\workflows\ci.yml = .github\workflows\ci.yml
test.cs = test.cs
.github\workflows\cd.yml = .github\workflows\cd.yml
README.md = README.md
.gitignore = .gitignore
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobloxCS.Tests", "RobloxCS.Tests\RobloxCS.Tests.csproj", "{D63E7B66-97C2-4948-8BAE-FA548C066DCE}"
Expand Down
12 changes: 6 additions & 6 deletions RobloxCS/Include/RuntimeLib.luau
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ function CS.getGlobal<T>(name: string): T -- TODO: if getting namespace call get
end

export type Class = {
__name: string; -- prob temporary
__className: string; -- prob temporary
} -- stub

function CS.is(object: any, class: Class | string): boolean
if typeof(class) == "table" and type(class.__name) == "string" then
return typeof(object) == "table" and typeof(object.__className) == "string" and object.__className == class.__name
if typeof(class) == "table" and typeof(class.__className) == "string" then
return typeof(object) == "table" and typeof(object.__className) == "string" and object.__className == class.__className
end

-- metatable check
Expand Down Expand Up @@ -56,7 +56,7 @@ self.Message = message
@native
function mt.__tostring(): string
return `{self["$className"]}: {self.Message}`
return `{self.__className}: {self.Message}`
end
@native
Expand All @@ -75,10 +75,10 @@ function CS.try(block: () -> nil, finallyBlock: () -> nil, catchBlocks: { CatchB
local success: boolean, ex: Exception | string | nil = pcall(block)
if not success then
if typeof(ex) == "string" then
ex = CS.getGlobal("Exception").new(ex, false)
ex = CS.getGlobal("Exception").new(ex, false) :: Exception
end
for _, catchBlock in catchBlocks do
if catchBlock.exceptionClass ~= nil and catchBlock.exceptionClass ~= (ex :: Exception)["$className"] then continue end
if catchBlock.exceptionClass ~= nil and catchBlock.exceptionClass ~= (ex :: Exception).__className then continue end
catchBlock.block(ex :: Exception, (ex :: Exception).Throw)
end
end
Expand Down
Loading

0 comments on commit 4cb66b3

Please sign in to comment.