├── .github
└── workflows
│ ├── cd.yml
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── RobloxCS.CLI
├── Program.cs
└── RobloxCS.CLI.csproj
├── RobloxCS.Luau
├── AST
│ ├── AST.cs
│ ├── AnonymousFunction.cs
│ ├── Argument.cs
│ ├── ArgumentList.cs
│ ├── ArrayType.cs
│ ├── Assignment.cs
│ ├── AssignmentTarget.cs
│ ├── AttributeList.cs
│ ├── BaseVariable.cs
│ ├── BinaryOperator.cs
│ ├── Block.cs
│ ├── Break.cs
│ ├── BuiltInAttribute.cs
│ ├── Call.cs
│ ├── Comment.cs
│ ├── Continue.cs
│ ├── ElementAccess.cs
│ ├── Expression.cs
│ ├── ExpressionStatement.cs
│ ├── FieldType.cs
│ ├── For.cs
│ ├── Function.cs
│ ├── FunctionType.cs
│ ├── GenericName.cs
│ ├── IdentifierName.cs
│ ├── If.cs
│ ├── IfExpression.cs
│ ├── IndexCall.cs
│ ├── InterfaceType.cs
│ ├── InterpolatedString.cs
│ ├── Interpolation.cs
│ ├── KeyOfCall.cs
│ ├── Literal.cs
│ ├── MappedType.cs
│ ├── MemberAccess.cs
│ ├── MultiLineComment.cs
│ ├── MultipleVariable.cs
│ ├── Name.cs
│ ├── NoOp.cs
│ ├── NoOpExpression.cs
│ ├── Node.cs
│ ├── NumericFor.cs
│ ├── OptionalType.cs
│ ├── Parameter.cs
│ ├── ParameterList.cs
│ ├── ParameterType.cs
│ ├── Parenthesized.cs
│ ├── QualifiedName.cs
│ ├── Repeat.cs
│ ├── Return.cs
│ ├── ScopedBlock.cs
│ ├── SimpleName.cs
│ ├── SingleLineComment.cs
│ ├── Statement.cs
│ ├── TableInitializer.cs
│ ├── TypeAlias.cs
│ ├── TypeCast.cs
│ ├── TypeOfCall.cs
│ ├── TypeRef.cs
│ ├── UnaryOperator.cs
│ ├── Variable.cs
│ ├── VariableList.cs
│ └── While.cs
├── AstUtility.cs
├── BaseWriter.cs
├── FileCompilation.cs
├── LuauWriter.cs
├── Macros.cs
├── OccupiedIdentifiersStack.cs
├── Prerequisites.cs
├── README.md
├── RobloxCS.Luau.csproj
└── SymbolMetadataManager.cs
├── RobloxCS.Shared
├── Analysis.cs
├── Config.cs
├── ConfigReader.cs
├── Constants.cs
├── FileUtility.cs
├── Logger.cs
├── RobloxCS.Shared.csproj
├── RojoReader.cs
└── StandardUtility.cs
├── RobloxCS.Tests
├── .lune
│ └── RuntimeLibTest.luau
├── AstUtilityTest.cs
├── Base
│ └── Generation.cs
├── GenerationTest.cs
├── LuauTests.cs
├── MacroTests
│ ├── ExtraTest.cs
│ ├── HashSetMacrosTest.cs
│ ├── ListMacrosTest.cs
│ └── ObjectMacrosTest.cs
├── RenderingTest.cs
├── RobloxCS.Tests.csproj
├── StandardUtilityTest.cs
├── TransformerTests
│ └── MainTransformerTest.cs
├── WholeFileRenderingTest.cs
├── lune
└── lune.exe
├── RobloxCS.sln
├── RobloxCS
├── Analyzer.cs
├── BaseGenerator.cs
├── Constants.cs
├── Include
│ ├── RuntimeLib.luau
│ └── Signal.luau
├── LuauGenerator.cs
├── README.md
├── RobloxCS.csproj
├── Transformers
│ ├── BaseTransformer.cs
│ ├── BuiltInTransformers.cs
│ └── MainTransformer.cs
├── Transpiler.cs
└── TranspilerUtility.cs
├── nuget.config
└── roblox-cs.png
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: CD
2 | on:
3 | pull_request:
4 | push:
5 | branches: [ "master" ]
6 |
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Setup .NET
13 | uses: actions/setup-dotnet@v4
14 | with:
15 | dotnet-version: 9.0.x
16 |
17 | - name: Install xmllint
18 | run: sudo apt-get update && sudo apt-get install -y libxml2-utils
19 |
20 | - name: Add RobloxCS GitHub packages source to NuGet
21 | run: dotnet nuget add source --store-password-in-clear-text -u roblox-csharp -p ${{ secrets.GITHUB_TOKEN }} -n rbxcs "https://nuget.pkg.github.com/roblox-csharp/index.json"
22 |
23 | - name: Extract version from .csproj
24 | id: extract_version
25 | run: |
26 | VERSION=$(xmllint --xpath "string(//Project/PropertyGroup/Version)" RobloxCS/RobloxCS.csproj)
27 | echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
28 |
29 | - name: Package the project
30 | run: dotnet pack -c Release
31 |
32 | - name: Run tests
33 | run: |
34 | chmod +x ./RobloxCS.Tests/lune
35 | dotnet test
36 |
37 | - name: Check if version is already published
38 | id: check_version
39 | run: |
40 | VERSION_EXISTS=$(dotnet nuget search roblox-cs --source "rbxcs" | grep -c "${{ env.PACKAGE_VERSION }}" || true)
41 | echo "VERSION_EXISTS=$VERSION_EXISTS" >> $GITHUB_ENV
42 |
43 | - name: Find and publish the latest package
44 | if: env.VERSION_EXISTS == '0'
45 | run: |
46 | PACKAGE_PATH=$(find ./RobloxCS/bin/Release -name "*.nupkg" -type f -print0 | xargs -0 ls -1t | head -n 1)
47 | dotnet nuget push "$PACKAGE_PATH" --api-key ${{ secrets.GITHUB_TOKEN }} --source "rbxcs" --skip-duplicate
48 |
49 | - name: Version already published
50 | if: env.VERSION_EXISTS != '0'
51 | run: echo "The version is already published. Skipping publish step."
52 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | pull_request:
4 | push:
5 | branches: [ "master" ]
6 |
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Setup .NET
13 | uses: actions/setup-dotnet@v4
14 | with:
15 | dotnet-version: 9.0.x
16 |
17 | - name: Add RobloxCS GitHub packages source to NuGet
18 | run: dotnet nuget add source --store-password-in-clear-text -u roblox-csharp -p ${{ secrets.GITHUB_TOKEN }} -n rbxcs "https://nuget.pkg.github.com/roblox-csharp/index.json"
19 |
20 | - name: Install ReportGenerator
21 | run: dotnet tool install --global dotnet-reportgenerator-globaltool --version 5.*
22 |
23 | - name: Configure PATH for tools
24 | run: echo "/home/runner/.dotnet/tools" >> $GITHUB_PATH
25 |
26 | - name: Run tests and generate coverage
27 | run: |
28 | chmod +x ./RobloxCS.Tests/lune
29 | dotnet test -c Release --collect:"XPlat Code Coverage"
30 |
31 | - name: Convert coverage to LCOV
32 | run: |
33 | reportgenerator -reports:"**/TestResults/**/*.cobertura.xml" -targetdir:"coverage" -reporttypes:lcov
34 | env:
35 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
36 | DOTNET_CLI_TELEMETRY_OPTOUT: true
37 |
38 | - name: Report coverage
39 | continue-on-error: true
40 | uses: coverallsapp/github-action@v2.3.4
41 | with:
42 | github-token: ${{ secrets.GITHUB_TOKEN }}
43 | file: coverage/lcov.info
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs/
2 | .idea/
3 | Properties/
4 |
5 | RobloxCS.Tests/**/out
6 | RobloxCS.Tests/**/include
7 | RobloxCS.Tests/**/package-lock.json
8 | *.DotSettings.user
9 | **/test.cs
10 | RobloxCS/GlobalUsings.cs
11 |
12 | obj/
13 | bin/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Riley "Runic" Peel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A C# to Luau transpiler for Roblox
5 |
6 |
7 |
13 |
14 | ### Introduction
15 |
16 | roblox-cs is a [C#](https://learn.microsoft.com/en-us/dotnet/csharp/) to [Luau](https://luau.org/) transpiler, which means we effectively translate C# code into
17 | Luau. This is done by taking the C# AST and converting it into a Luau AST (that is functionally the same) and then finally rendering the Luau AST into Luau
18 | source code.
19 |
20 | ### Examples
21 |
22 | #### Hello, world!
23 |
24 | ```cs
25 | print("Hello, roblox-cs!");
26 | ```
27 | ```luau
28 | -- Compiled with roblox-cs v2.0.0
29 |
30 | print("Hello, roblox-cs!")
31 | return nil
32 | ```
33 |
34 | #### Classes
35 | **Note:** In the future this example will import the `CS` library. It will also probably abandon `typeof()`.
36 |
37 | ```cs
38 | var myClass = new MyClass(69);
39 | myClass.DoSomething();
40 | print(myClass.MyProperty); // 69
41 | print(myClass.MyField); // 0
42 |
43 | class MyClass(int value)
44 | {
45 | public readonly int MyField;
46 | public int MyProperty { get; } = value;
47 |
48 | public void DoSomething() =>
49 | print("doing something!");
50 | }
51 | ```
52 | ```luau
53 | -- Compiled with roblox-cs v2.0.0
54 |
55 | local MyClass
56 | do
57 | MyClass = setmetatable({}, {
58 | __tostring = function(): string
59 | return "MyClass"
60 | end
61 | })
62 | MyClass.__index = MyClass
63 | MyClass.__className = "MyClass"
64 | function MyClass.new(value: number): MyClass
65 | local self = (setmetatable({}, MyClass) :: any) :: MyClass
66 | return self:MyClass(value) or self
67 | end
68 | function MyClass:DoSomething(): ()
69 | return print("doing something!")
70 | end
71 | function MyClass:MyClass(value: number): MyClass?
72 | return nil
73 | end
74 | end
75 | CS.defineGlobal("MyClass", MyClass)
76 | type MyClass = typeof(MyClass)
77 |
78 | local myClass = MyClass.new(69)
79 | myClass:DoSomething()
80 | print(myClass.MyProperty)
81 | print(myClass.MyField)
82 | return nil
83 | ```
84 |
85 | #### Type Reflection
86 | **Note:** `Object.GetType()` is not supported.
87 |
88 | ```cs
89 | var intType = typeof(int);
90 | print(intType.Name); // Int32
91 | print(intType.Namespace); // System
92 | print(intType.BaseType.Name); // ValueType
93 | ```
94 | ```luau
95 | local intType = { --[[ insert type info here ]] };
96 | print(intType.Name);
97 | print(intType.Namespace);
98 | print(intType.BaseType.Name);
99 | ```
100 |
101 | ### Join the Community!
102 |
103 |
--------------------------------------------------------------------------------
/RobloxCS.CLI/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Xml.Linq;
3 | using CommandLine;
4 | using RobloxCS;
5 | using RobloxCS.Shared;
6 |
7 | Parser.Default
8 | .ParseArguments(args)
9 | .WithParsed(HandleOptions);
10 |
11 | static void HandleOptions(Options opts)
12 | {
13 | if (opts.Version)
14 | {
15 | var assembly = typeof(Transpiler).Assembly;
16 | var informationalVersionAttribute = assembly.GetCustomAttribute();
17 | Console.WriteLine(informationalVersionAttribute?.InformationalVersion.Split('+').First());
18 | return;
19 | }
20 |
21 | if (opts.SingleFile != null)
22 | {
23 | var transpiledLuau = Transpiler.TranspileSources([opts.SingleFile],
24 | new RojoProject(),
25 | ConfigReader.UnitTestingConfig);
26 |
27 | Console.WriteLine(transpiledLuau.First().Output);
28 | return;
29 | }
30 |
31 | if (!Directory.Exists(opts.ProjectDirectory))
32 | throw Logger.Error($"Project directory does not exist at '{opts.ProjectDirectory}'");
33 |
34 | var config = ConfigReader.Read(opts.ProjectDirectory);
35 | Transpiler.Transpile(opts.ProjectDirectory, config, opts.Verbose);
36 | }
37 |
38 | internal class Options
39 | {
40 | [Option('v', "version",
41 | Required = false,
42 | HelpText = "Returns the compiler version.")]
43 | public required bool Version { get; init; }
44 |
45 | [Option("verbose",
46 | Required = false,
47 | HelpText = "Verbosely outputs transpilation process.")]
48 | public required bool Verbose { get; init; }
49 |
50 | [Option('f', "single-file",
51 | Required = false,
52 | HelpText = "Transpiles a single file and spits the emitted Luau out into the console.")]
53 | public required string? SingleFile { get; init; }
54 |
55 | [Option('p', "project",
56 | Required = false,
57 | HelpText = "Explicitly specify the project directory to compile. If none specified, automatically attempts to locate one.")]
58 | public required string ProjectDirectory { get; init; } = ".";
59 | }
--------------------------------------------------------------------------------
/RobloxCS.CLI/RobloxCS.CLI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/AST.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class AST : Node
4 | {
5 | public List Statements { get; }
6 |
7 | public AST(List statements)
8 | {
9 | Statements = statements;
10 | AddChildren(Statements);
11 | }
12 |
13 | public override void Render(LuauWriter luau)
14 | {
15 | foreach (var statement in Statements) statement.Render(luau);
16 |
17 | luau.WriteReturn();
18 | }
19 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/AnonymousFunction.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class AnonymousFunction : Expression
4 | {
5 | public ParameterList ParameterList { get; }
6 | public Block? Body { get; }
7 | public TypeRef? ReturnType { get; }
8 | public List AttributeLists { get; }
9 | public List? TypeParameters { get; }
10 |
11 | public AnonymousFunction(ParameterList parameterList,
12 | TypeRef? returnType = null,
13 | Block? body = null,
14 | List? attributeLists = null,
15 | List? typeParameters = null)
16 | {
17 | ParameterList = parameterList;
18 | Body = body;
19 | TypeParameters = typeParameters;
20 | Body = body;
21 | ReturnType = returnType;
22 | AttributeLists = attributeLists ?? [];
23 | AddChild(ParameterList);
24 | if (ReturnType != null) AddChild(ReturnType);
25 | if (Body != null) AddChild(Body);
26 |
27 | AddChildren(AttributeLists);
28 | if (typeParameters != null) AddChildren(typeParameters);
29 | }
30 |
31 | public override void Render(LuauWriter luau) =>
32 | luau.WriteFunction(null,
33 | false,
34 | ParameterList,
35 | ReturnType,
36 | Body,
37 | AttributeLists,
38 | inlineAttributes: true,
39 | createNewline: false);
40 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Argument.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class Argument : Expression
4 | {
5 | public Expression Expression { get; }
6 |
7 | public Argument(Expression expression)
8 | {
9 | Expression = expression;
10 | AddChild(Expression);
11 | }
12 |
13 | public override void Render(LuauWriter luau) => Expression.Render(luau);
14 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/ArgumentList.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class ArgumentList : Expression
4 | {
5 | public static readonly ArgumentList Empty = new([]);
6 |
7 | public ArgumentList(List arguments)
8 | {
9 | Arguments = arguments;
10 | AddChildren(Arguments);
11 | }
12 |
13 | public List Arguments { get; }
14 |
15 | public override void Render(LuauWriter luau)
16 | {
17 | luau.Write('(');
18 | luau.WriteNodesCommaSeparated(Arguments);
19 | luau.Write(')');
20 | }
21 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/ArrayType.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class ArrayType : TypeRef
4 | {
5 | public TypeRef ElementType { get; }
6 |
7 | public ArrayType(TypeRef elementType)
8 | : base("{ " + elementType.Path + " }", true)
9 | {
10 | ElementType = elementType;
11 | AddChild(ElementType);
12 | }
13 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Assignment.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public sealed class Assignment : Statement
4 | {
5 | public AssignmentTarget Target { get; }
6 | public Expression Value { get; }
7 |
8 | public Assignment(AssignmentTarget target, Expression value)
9 | {
10 | Target = target;
11 | Value = value;
12 | AddChildren([Target, Value]);
13 | }
14 |
15 | public override void Render(LuauWriter luau) => luau.WriteAssignment(Target, Value);
16 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/AssignmentTarget.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public abstract class AssignmentTarget : Expression;
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/AttributeList.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class AttributeList : Statement
4 | {
5 | public List Attributes { get; }
6 | public bool Inline { get; set; } = false;
7 |
8 | public AttributeList(List attributes)
9 | {
10 | Attributes = attributes;
11 | AddChildren(Attributes);
12 | }
13 |
14 | public override void Render(LuauWriter luau)
15 | {
16 | foreach (var attributeNode in Attributes)
17 | {
18 | if (attributeNode is BuiltInAttribute attribute)
19 | {
20 | attribute.Render(luau);
21 |
22 | if (Inline || attribute.Inline) continue;
23 |
24 | luau.WriteLine();
25 | }
26 | else
27 | {
28 | // TODO: user-defined attribute stuff
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/BaseVariable.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public abstract class BaseVariable : Statement
4 | {
5 | protected BaseVariable(bool isLocal, TypeRef? type)
6 | {
7 | IsLocal = isLocal;
8 | Type = type;
9 | if (Type != null) AddChild(Type);
10 | }
11 |
12 | public bool IsLocal { get; }
13 | public TypeRef? Type { get; }
14 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/BinaryOperator.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class BinaryOperator : Expression
4 | {
5 | public Expression Left { get; }
6 | public string Operator { get; }
7 | public Expression Right { get; }
8 |
9 | public BinaryOperator(Expression left, string @operator, Expression right)
10 | {
11 | Left = left;
12 | Operator = @operator;
13 | Right = right;
14 | AddChildren([Left, Right]);
15 | }
16 |
17 | public override void Render(LuauWriter luau)
18 | {
19 | Left.Render(luau);
20 | luau.Write($" {Operator} ");
21 | Right.Render(luau);
22 | }
23 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Block.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class Block : Statement
4 | {
5 | public Block(List statements)
6 | {
7 | Statements = statements;
8 | AddChildren(Statements);
9 | }
10 |
11 | public List Statements { get; }
12 |
13 | public override void Render(LuauWriter luau) => luau.WriteNodes(Statements);
14 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Break.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class Break : Statement
4 | {
5 | public override void Render(LuauWriter luau) => luau.WriteLine("break");
6 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/BuiltInAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class BuiltInAttribute : Statement
4 | {
5 | public Name Name { get; }
6 | public bool Inline { get; }
7 |
8 | public BuiltInAttribute(Name name, bool inline = false)
9 | {
10 | Name = name;
11 | Inline = inline;
12 | AddChild(name);
13 | }
14 |
15 | public override void Render(LuauWriter luau)
16 | {
17 | luau.Write('@');
18 | Name.Render(luau);
19 | }
20 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Call.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public sealed class Call : Expression
4 | {
5 | public Call(Expression callee, ArgumentList? argumentList = null)
6 | {
7 | // monkey patch for https://github.com/roblox-csharp/roblox-cs/issues/44
8 | if (callee is Name name) callee = AstUtility.GetNonGenericName(name);
9 |
10 | Callee = callee;
11 | ArgumentList = argumentList ?? ArgumentList.Empty;
12 | AddChildren([Callee, ArgumentList]);
13 | }
14 |
15 | public Expression Callee { get; }
16 | public ArgumentList ArgumentList { get; }
17 |
18 | public override void Render(LuauWriter luau)
19 | {
20 | Callee.Render(luau);
21 | luau.Write('(');
22 | luau.WriteNodesCommaSeparated(ArgumentList.Arguments);
23 | luau.Write(')');
24 | }
25 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Comment.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public abstract class Comment(string contents) : Statement
4 | {
5 | public string Contents { get; } = contents;
6 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Continue.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class Continue : Statement
4 | {
5 | public override void Render(LuauWriter luau) => luau.WriteLine("continue");
6 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/ElementAccess.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public sealed class ElementAccess : AssignmentTarget
4 | {
5 | public Expression Expression { get; }
6 | public Expression Index { get; set; }
7 |
8 | public ElementAccess(Expression expression, Expression index)
9 | {
10 | Expression = expression;
11 | Index = index;
12 | AddChildren([Expression, Index]);
13 | }
14 |
15 | public override void Render(LuauWriter luau)
16 | {
17 | Expression.Render(luau);
18 | luau.Write('[');
19 | Index.Render(luau);
20 | luau.Write(']');
21 | }
22 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Expression.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public abstract class Expression : Node;
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/ExpressionStatement.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class ExpressionStatement : Statement
4 | {
5 | public Expression Expression { get; }
6 |
7 | public ExpressionStatement(Expression expression)
8 | {
9 | Expression = expression;
10 | AddChild(Expression);
11 | }
12 |
13 | public override void Render(LuauWriter luau)
14 | {
15 | Expression.Render(luau);
16 |
17 | if (Expression is NoOpExpression) return;
18 |
19 | luau.WriteLine();
20 | }
21 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/FieldType.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class FieldType : TypeRef
4 | {
5 | public string Name { get; }
6 | public TypeRef ValueType { get; }
7 | public bool IsReadOnly { get; }
8 |
9 | public FieldType(string name, TypeRef valueType, bool isReadOnly)
10 | : base($"{(isReadOnly ? "read " : "")}{name}: {valueType.Path};", true)
11 | {
12 | Name = name;
13 | ValueType = valueType;
14 | IsReadOnly = isReadOnly;
15 | AddChild(ValueType);
16 | }
17 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/For.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class For : Statement
4 | {
5 | public List Names { get; }
6 | public Expression Iterable { get; }
7 | public Statement Body { get; }
8 |
9 | public For(List names, Expression iterable, Statement body)
10 | {
11 | Names = names;
12 | Iterable = iterable;
13 | Body = body;
14 | AddChildren(Names);
15 | AddChild(Iterable);
16 | AddChild(Body);
17 | }
18 |
19 | public override void Render(LuauWriter luau)
20 | {
21 | luau.Write("for ");
22 | luau.WriteNodesCommaSeparated(Names);
23 |
24 | luau.Write(" in ");
25 | Iterable.Render(luau);
26 | luau.WriteLine(" do");
27 | luau.PushIndent();
28 |
29 | Body.Render(luau);
30 | luau.PopIndent();
31 | luau.WriteLine("end");
32 | }
33 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Function.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class Function : Statement
4 | {
5 | public Name Name { get; }
6 | public bool IsLocal { get; }
7 | public ParameterList ParameterList { get; }
8 | public Block? Body { get; }
9 | public TypeRef? ReturnType { get; }
10 | public List AttributeLists { get; }
11 | public List? TypeParameters { get; }
12 |
13 | public Function(Name name,
14 | bool isLocal,
15 | ParameterList parameterList,
16 | TypeRef? returnType = null,
17 | Block? body = null,
18 | List? attributeLists = null,
19 | List? typeParameters = null)
20 | {
21 | Name = name;
22 | IsLocal = isLocal;
23 | ParameterList = parameterList;
24 | Body = body;
25 | ReturnType = returnType;
26 | AttributeLists = attributeLists ?? [];
27 | TypeParameters = typeParameters;
28 |
29 | AddChild(Name);
30 | AddChild(ParameterList);
31 | if (ReturnType != null) AddChild(ReturnType);
32 | if (Body != null) AddChild(Body);
33 |
34 | AddChildren(AttributeLists);
35 | if (typeParameters != null) AddChildren(typeParameters);
36 | }
37 |
38 | public override void Render(LuauWriter luau) =>
39 | luau.WriteFunction(Name,
40 | IsLocal,
41 | ParameterList,
42 | ReturnType,
43 | Body,
44 | AttributeLists,
45 | TypeParameters);
46 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/FunctionType.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class FunctionType : TypeRef
4 | {
5 | public static readonly FunctionType NoOp = new([], new TypeRef("()"));
6 |
7 | public FunctionType(List parameterTypes, TypeRef returnType)
8 | : base("", true)
9 | {
10 | ParameterTypes = parameterTypes;
11 | ReturnType = returnType;
12 | Path = $"({string.Join(", ", parameterTypes.Select(type => type.Path))}) -> {returnType.Path}";
13 | }
14 |
15 | public List ParameterTypes { get; }
16 | public TypeRef ReturnType { get; }
17 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/GenericName.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class GenericName(string text, List typeArguments) : SimpleName
4 | {
5 | public string Text { get; } = text;
6 | public List TypeArguments { get; } = typeArguments;
7 |
8 | public override void Render(LuauWriter luau) => luau.Write(ToString());
9 |
10 | public override string ToString() => Text + '<' + string.Join(", ", TypeArguments) + '>';
11 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/IdentifierName.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class IdentifierName(string text) : SimpleName
4 | {
5 | public string Text { get; } = text;
6 |
7 | public override void Render(LuauWriter luau) => luau.Write(Text);
8 |
9 | public override string ToString() => Text;
10 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/If.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class If : Statement
4 | {
5 | public Expression Condition { get; }
6 | public Block Body { get; }
7 | public Block? ElseBranch { get; }
8 |
9 | public If(Expression condition, Block body, Block? elseBranch = null)
10 | {
11 | Condition = condition;
12 | Body = body;
13 | ElseBranch = elseBranch;
14 |
15 | AddChildren([Condition, Body]);
16 | if (ElseBranch != null) AddChild(ElseBranch);
17 | }
18 |
19 | public override void Render(LuauWriter luau)
20 | {
21 | luau.Write("if ");
22 | Condition.Render(luau);
23 | luau.Write(" then");
24 |
25 | var compact = ElseBranch == null
26 | && Body.Statements.Count == 1
27 | && Body.Statements.First() is Return { Expression: null or Literal { ValueText: "nil" } } or Break or Continue;
28 |
29 | luau.Write(compact ? ' ' : '\n');
30 | if (!compact) luau.PushIndent();
31 |
32 | (compact ? Body.Statements.First() : Body).Render(luau);
33 | if (compact)
34 | {
35 | luau.Remove(1);
36 | luau.Write(' ');
37 | }
38 |
39 | var isElseIf = ElseBranch is { Statements.Count: 1 } && ElseBranch.Statements.First() is If;
40 | if (ElseBranch != null)
41 | {
42 | luau.PopIndent();
43 | luau.Write("else" + (isElseIf ? "" : '\n'));
44 | if (!isElseIf) luau.PushIndent();
45 |
46 | ElseBranch.Render(luau);
47 | }
48 |
49 | if (!compact) luau.PopIndent();
50 |
51 | if (isElseIf) return;
52 |
53 | luau.WriteLine("end");
54 | }
55 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/IfExpression.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class IfExpression : Expression
4 | {
5 | public Expression Condition { get; }
6 | public Expression Body { get; }
7 | public Expression? ElseBranch { get; }
8 | public bool IsCompact { get; }
9 |
10 | public IfExpression(Expression condition, Expression body, Expression? elseBranch = null, bool isCompact = false)
11 | {
12 | Condition = condition;
13 | Body = body;
14 | ElseBranch = elseBranch;
15 | IsCompact = isCompact;
16 |
17 | AddChild(Condition);
18 | AddChild(Body);
19 | if (ElseBranch != null) AddChild(ElseBranch);
20 | }
21 |
22 | public override void Render(LuauWriter luau)
23 | {
24 | luau.Write("if ");
25 | Condition.Render(luau);
26 | luau.Write(" then");
27 | luau.Write(IsCompact ? ' ' : '\n');
28 | if (!IsCompact) luau.PushIndent();
29 |
30 | Node body = IsCompact ? Body : new ExpressionStatement(Body); // for the newline
31 | body.Render(luau);
32 |
33 | var isElseIf = ElseBranch is IfExpression;
34 |
35 | if (ElseBranch == null) return;
36 |
37 | if (IsCompact)
38 | luau.Write(' ');
39 | else
40 | luau.PopIndent();
41 |
42 | luau.Write("else");
43 | if (IsCompact && !isElseIf) luau.Write(' ');
44 | if (!isElseIf && !IsCompact)
45 | {
46 | luau.WriteLine();
47 | luau.PushIndent();
48 | }
49 |
50 | Node elseBranch = IsCompact ? ElseBranch : new ExpressionStatement(ElseBranch);
51 | elseBranch.Render(luau);
52 | if (elseBranch is Statement) luau.Remove(1);
53 |
54 | if (isElseIf || IsCompact) return;
55 |
56 | luau.PopIndent();
57 | }
58 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/IndexCall.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class IndexCall(TypeRef typeRef, TypeRef key)
4 | : TypeRef(typeRef.Path)
5 | {
6 | public TypeRef TypeRef { get; } = typeRef;
7 | public TypeRef Key { get; } = key;
8 |
9 | public override void Render(LuauWriter luau)
10 | {
11 | luau.Write("index<");
12 | TypeRef.Render(luau);
13 | luau.Write(", ");
14 | Key.Render(luau);
15 | luau.Write(">");
16 | }
17 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/InterfaceType.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class InterfaceType : TypeRef
4 | {
5 | public HashSet Fields { get; }
6 | public MappedType? ExtraMapping { get; }
7 | public bool IsCompact { get; }
8 |
9 | public InterfaceType(HashSet fields, MappedType? extraMapping = null, bool isCompact = true)
10 | : base("", true)
11 | {
12 | Fields = fields;
13 | ExtraMapping = extraMapping;
14 | IsCompact = Fields.Count == 0 && isCompact;
15 | Path = ToString();
16 | }
17 |
18 | public string ToString(int indent = 0)
19 | {
20 | var tabsOutside = new string(' ', indent * BaseWriter.IndentSize);
21 | var tabsInside = new string(' ', (indent + 1) * BaseWriter.IndentSize);
22 | var newline = IsCompact ? "" : "\n";
23 |
24 | return tabsOutside
25 | + "{"
26 | + newline
27 | + string.Join(newline, Fields.Select(field => tabsInside + field.Path))
28 | + tabsOutside
29 | + newline
30 | + "}";
31 | }
32 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/InterpolatedString.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class InterpolatedString : Expression
4 | {
5 | public List Parts { get; }
6 |
7 | public InterpolatedString(List parts)
8 | {
9 | Parts = parts;
10 | AddChildren(Parts);
11 | }
12 |
13 | public override void Render(LuauWriter luau)
14 | {
15 | luau.Write('`');
16 | foreach (var part in Parts) part.Render(luau);
17 |
18 | luau.Write('`');
19 | }
20 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Interpolation.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | ///
4 | /// An interpolated section of an
5 | ///
6 | public class Interpolation : Expression
7 | {
8 | public Expression Expression { get; }
9 |
10 | public Interpolation(Expression expression)
11 | {
12 | Expression = expression;
13 | AddChild(Expression);
14 | }
15 |
16 | public override void Render(LuauWriter luau)
17 | {
18 | luau.Write('{');
19 | Expression.Render(luau);
20 | luau.Write('}');
21 | }
22 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/KeyOfCall.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public sealed class KeyOfCall : TypeRef
4 | {
5 | public TypeRef TypeRef { get; }
6 |
7 | public KeyOfCall(TypeRef typeRef)
8 | : base(typeRef.Path)
9 | {
10 | TypeRef = typeRef;
11 | AddChild(TypeRef);
12 | }
13 |
14 | public override void Render(LuauWriter luau)
15 | {
16 | luau.Write("keyof<");
17 | TypeRef.Render(luau);
18 | luau.Write(">");
19 | }
20 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Literal.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class Literal(string valueText) : Expression
4 | {
5 | public string ValueText { get; } = valueText;
6 |
7 | public override void Render(LuauWriter luau) => luau.Write(ValueText);
8 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/MappedType.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class MappedType : TypeRef
4 | {
5 | public TypeRef KeyType { get; }
6 | public TypeRef ValueType { get; }
7 |
8 | public MappedType(TypeRef keyType, TypeRef valueType)
9 | : base($"{{ [{keyType.Path}]: " + valueType.Path + " }", true)
10 | {
11 | KeyType = keyType;
12 | ValueType = valueType;
13 | AddChildren([KeyType, ValueType]);
14 | }
15 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/MemberAccess.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class MemberAccess : AssignmentTarget
4 | {
5 | public MemberAccess(Expression expression, SimpleName name, char @operator = '.')
6 | {
7 | Expression = expression;
8 | Operator = @operator;
9 | Name = name;
10 | AddChildren([Expression, Name]);
11 | }
12 |
13 | public Expression Expression { get; }
14 | public char Operator { get; }
15 | public SimpleName Name { get; }
16 |
17 | public override void Render(LuauWriter luau)
18 | {
19 | Expression.Render(luau);
20 | luau.Write(Operator);
21 | Name.Render(luau);
22 | }
23 |
24 | public MemberAccess WithOperator(char @operator) => new(Expression, Name, @operator);
25 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/MultiLineComment.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class MultiLineComment(string contents)
4 | : Comment(contents)
5 | {
6 | public override void Render(LuauWriter luau)
7 | {
8 | luau.WriteLine("--[[");
9 | luau.WriteLine(Contents);
10 | luau.WriteLine("]]");
11 | }
12 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/MultipleVariable.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public sealed class MultipleVariable : BaseVariable
4 | {
5 | public MultipleVariable(HashSet names, bool isLocal, List initializers, TypeRef? type = null)
6 | : base(isLocal, type)
7 | {
8 | Names = names;
9 | Initializers = initializers;
10 |
11 | AddChildren(Names);
12 | AddChildren(Initializers);
13 | }
14 |
15 | public HashSet Names { get; }
16 | public List Initializers { get; }
17 |
18 | public override void Render(LuauWriter luau) => luau.WriteVariable(Names, IsLocal, Initializers, Type);
19 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Name.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public abstract class Name : AssignmentTarget
4 | {
5 | public abstract override string ToString();
6 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/NoOp.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | /// Optionally renders a newline.
4 | public sealed class NoOp(bool createNewline = true) : Statement
5 | {
6 | public override void Render(LuauWriter luau)
7 | {
8 | if (!createNewline) return;
9 |
10 | luau.WriteLine();
11 | }
12 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/NoOpExpression.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | /// Only meant for use with macros.
4 | public sealed class NoOpExpression : Expression
5 | {
6 | public override void Render(LuauWriter luau)
7 | {
8 | }
9 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Node.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Macros;
2 | using RobloxCS.Shared;
3 |
4 | namespace RobloxCS.Luau;
5 |
6 | public abstract class Node
7 | {
8 | public Node? Parent { get; private set; }
9 | public List Children { get; } = [];
10 | public MacroKind? ExpandedByMacro { get; private set; }
11 |
12 | private List? _descendants;
13 |
14 | public List Descendants
15 | {
16 | get
17 | {
18 | if (_descendants != null) return _descendants;
19 |
20 | _descendants = [];
21 | foreach (var child in Children)
22 | {
23 | _descendants.Add(child);
24 | _descendants.AddRange(child.Descendants);
25 | }
26 |
27 | return _descendants;
28 | }
29 | set => _descendants = value;
30 | }
31 |
32 | public abstract void Render(LuauWriter luau);
33 |
34 | public void MarkExpanded(MacroKind macroKind)
35 | {
36 | if (ExpandedByMacro != null)
37 | throw Logger.CompilerError($"""
38 | Attempted to mark already macro-expanded node as expanded.
39 | Current macro kind: {ExpandedByMacro}
40 | Attempted expanding macro kind: {macroKind}
41 | """.Trim());
42 |
43 | ExpandedByMacro = macroKind;
44 | }
45 |
46 | protected void AddChild(Node child)
47 | {
48 | child.Parent = this;
49 | Children.Add(child);
50 | }
51 |
52 | protected void AddChildren(IEnumerable children)
53 | {
54 | foreach (var child in children) AddChild(child);
55 | }
56 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/NumericFor.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class NumericFor : Statement
4 | {
5 | public IdentifierName Name { get; }
6 | public Expression Minimum { get; }
7 | public Expression Maximum { get; }
8 | public Expression? IncrementBy { get; }
9 | public Statement Body { get; }
10 |
11 | public NumericFor(IdentifierName name, Expression minimum, Expression maximum, Expression? incrementBy, Statement body)
12 | {
13 | Name = name;
14 | Minimum = minimum;
15 | Maximum = maximum;
16 | IncrementBy = incrementBy;
17 | Body = body;
18 |
19 | AddChildren([Name, Minimum, Maximum]);
20 | if (IncrementBy != null) AddChild(IncrementBy);
21 |
22 | AddChild(Body);
23 | }
24 |
25 | public override void Render(LuauWriter luau)
26 | {
27 | luau.Write("for ");
28 | Name.Render(luau);
29 | luau.Write(" = ");
30 | Minimum.Render(luau);
31 | luau.Write(", ");
32 | Maximum.Render(luau);
33 | if (IncrementBy != null)
34 | {
35 | luau.Write(", ");
36 | IncrementBy.Render(luau);
37 | }
38 |
39 | luau.WriteLine(" do");
40 | luau.PushIndent();
41 |
42 | Body.Render(luau);
43 |
44 | luau.PopIndent();
45 | luau.WriteLine("end");
46 | }
47 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/OptionalType.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class OptionalType : TypeRef
4 | {
5 | public TypeRef NonNullableType { get; }
6 |
7 | public OptionalType(TypeRef nonNullableType)
8 | : base(nonNullableType.Path + "?", true)
9 | {
10 | NonNullableType = nonNullableType;
11 | AddChild(nonNullableType);
12 | }
13 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Parameter.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class Parameter : Statement
4 | {
5 | public IdentifierName Name { get; }
6 | public Expression? Initializer { get; }
7 | public TypeRef? Type { get; }
8 | public bool IsVararg { get; }
9 |
10 | // parameter initializers are not true to luau, but we have not used prerequisite statements anywhere yet. so when we do, this will likely be phased out.
11 | public Parameter(IdentifierName name, bool isVararg = false, Expression? initializer = null, TypeRef? type = null)
12 | {
13 | Name = name;
14 | Initializer = initializer;
15 | Type = type;
16 | IsVararg = isVararg;
17 |
18 | AddChild(Name);
19 | if (Initializer != null) AddChild(Initializer);
20 | if (Type != null) AddChild(FixType(Type));
21 | }
22 |
23 | public override void Render(LuauWriter luau)
24 | {
25 | if (IsVararg)
26 | luau.Write("...");
27 | else
28 | Name.Render(luau);
29 |
30 | if (Type == null) return;
31 |
32 | luau.Write(": ");
33 | Type.Render(luau);
34 | }
35 |
36 | private TypeRef FixType(TypeRef type)
37 | {
38 | while (true)
39 | {
40 | if (type is ArrayType arrayType && IsVararg)
41 | {
42 | type = arrayType.ElementType;
43 |
44 | continue;
45 | }
46 |
47 | var isOptional = type is OptionalType;
48 | if (Initializer != null || isOptional)
49 | {
50 | return isOptional ? type : new OptionalType(type);
51 | }
52 |
53 | return type;
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/ParameterList.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class ParameterList : Statement
4 | {
5 | public static readonly ParameterList Empty = new([]);
6 |
7 | public ParameterList(List parameters)
8 | {
9 | Parameters = parameters;
10 | AddChildren(Parameters);
11 | }
12 |
13 | public List Parameters { get; }
14 |
15 | public override void Render(LuauWriter luau)
16 | {
17 | luau.Write('(');
18 | luau.WriteNodesCommaSeparated(Parameters);
19 | luau.Write(')');
20 | }
21 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/ParameterType.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class ParameterType : TypeRef
4 | {
5 | public string? Name { get; }
6 | public TypeRef Type { get; }
7 |
8 | public ParameterType(string? name, TypeRef type)
9 | : base(name != null ? $"{name}: {type.Path}" : type.Path, true)
10 | {
11 | Name = name;
12 | Type = type;
13 | }
14 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Parenthesized.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class Parenthesized : Expression
4 | {
5 | public Expression Expression { get; }
6 |
7 | public Parenthesized(Expression expression)
8 | {
9 | Expression = expression;
10 | }
11 |
12 | public override void Render(LuauWriter luau)
13 | {
14 | luau.Write('(');
15 | Expression.Render(luau);
16 | luau.Write(')');
17 | }
18 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/QualifiedName.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class QualifiedName : Name
4 | {
5 | public QualifiedName(Name left, SimpleName right, char @operator = '.')
6 | {
7 | Left = left;
8 | Right = right;
9 | Operator = @operator;
10 | AddChildren([Left, Right]);
11 | }
12 |
13 | public Name Left { get; }
14 | public char Operator { get; }
15 | public SimpleName Right { get; }
16 |
17 | public override void Render(LuauWriter luau)
18 | {
19 | Left.Render(luau);
20 | luau.Write(Operator);
21 | Right.Render(luau);
22 | }
23 |
24 | public override string ToString() => Left.ToString() + Operator + Right;
25 |
26 | public QualifiedName WithOperator(char @operator) => new(Left, Right, @operator);
27 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Repeat.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class Repeat : Statement
4 | {
5 | public Expression UntilCondition { get; }
6 | public Statement Body { get; }
7 |
8 | public Repeat(Expression untilCondition, Statement body)
9 | {
10 | UntilCondition = untilCondition;
11 | Body = body;
12 | AddChildren([UntilCondition, Body]);
13 | }
14 |
15 | public override void Render(LuauWriter luau)
16 | {
17 | luau.WriteLine("repeat ");
18 | luau.PushIndent();
19 | Body.Render(luau);
20 | luau.PopIndent();
21 | luau.Write("until ");
22 | UntilCondition.Render(luau);
23 | luau.WriteLine(""); // is there a better way to add a new line?
24 | }
25 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Return.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau
2 | {
3 | public class Return : Statement
4 | {
5 | public Expression? Expression { get; }
6 |
7 | public Return(Expression? expression = null)
8 | {
9 | Expression = expression;
10 | if (Expression != null) AddChild(Expression);
11 | }
12 |
13 | public override void Render(LuauWriter luau) => luau.WriteReturn(Expression);
14 | }
15 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/ScopedBlock.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class ScopedBlock(List statements)
4 | : Block(statements)
5 | {
6 | public override void Render(LuauWriter luau)
7 | {
8 | luau.WriteLine("do");
9 | luau.PushIndent();
10 | base.Render(luau);
11 | luau.PopIndent();
12 | luau.WriteLine("end");
13 | }
14 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/SimpleName.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | ///
4 | /// A name that is singular and not a combination of multiple names
5 | ///
6 | public abstract class SimpleName : Name
7 | {
8 | public abstract override string ToString();
9 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/SingleLineComment.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class SingleLineComment(string contents)
4 | : Comment(contents)
5 | {
6 | public override void Render(LuauWriter luau)
7 | {
8 | luau.Write("-- ");
9 | luau.Write(Contents);
10 | }
11 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Statement.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public abstract class Statement : Node;
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/TableInitializer.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Shared;
2 |
3 | namespace RobloxCS.Luau;
4 |
5 | public class TableInitializer : Expression
6 | {
7 | public static readonly TableInitializer Empty = new();
8 |
9 | public TableInitializer(List? values = null,
10 | List? keys = null)
11 | {
12 | Values = values ?? [];
13 | Keys = keys ?? [];
14 |
15 | KeyValuePairs = [];
16 | for (var i = 0; i < Math.Max(Values.Count, Keys.Count); i++)
17 | {
18 | var key = Keys.ElementAtOrDefault(i);
19 | var value = Values.ElementAtOrDefault(i);
20 |
21 | if (key == null || value == null) continue;
22 |
23 | KeyValuePairs.Add(KeyValuePair.Create(key, value));
24 | }
25 |
26 | AddChildren(Values);
27 | AddChildren(Keys);
28 | }
29 |
30 | public List Values { get; }
31 | public List Keys { get; }
32 | public List> KeyValuePairs { get; }
33 |
34 | public static TableInitializer Union(TableInitializer a, TableInitializer b)
35 | {
36 | var kvpComparer = new StandardUtility.KeyValuePairEqualityComparer();
37 | var pairs = a.KeyValuePairs.Union(b.KeyValuePairs, kvpComparer).ToDictionary();
38 |
39 | return new TableInitializer(pairs.Values.ToList(), pairs.Keys.ToList());
40 | }
41 |
42 | public override void Render(LuauWriter luau)
43 | {
44 | var hasAnyKeys = Keys.Count > 0;
45 |
46 | luau.Write('{');
47 | if (hasAnyKeys)
48 | {
49 | luau.WriteLine();
50 | luau.PushIndent();
51 | }
52 |
53 | for (var i = 0; i < Values.Count; i++)
54 | {
55 | var value = Values[i];
56 | var key = Keys.ElementAtOrDefault(i);
57 | if (key != null)
58 | {
59 | if (key is not IdentifierName) luau.Write('[');
60 |
61 | key.Render(luau);
62 | if (key is not IdentifierName) luau.Write(']');
63 |
64 | luau.Write(" = ");
65 | }
66 |
67 | value.Render(luau);
68 |
69 | if (i == Values.Count - 1) continue;
70 |
71 | luau.Write(',');
72 | luau.Write(hasAnyKeys ? '\n' : ' ');
73 | }
74 |
75 | if (hasAnyKeys)
76 | {
77 | luau.PopIndent();
78 | luau.WriteLine();
79 | }
80 |
81 | luau.Write('}');
82 | }
83 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/TypeAlias.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public sealed class TypeAlias : Statement
4 | {
5 | public SimpleName Name { get; }
6 | public TypeRef Type { get; }
7 |
8 | public TypeAlias(SimpleName name, TypeRef type)
9 | {
10 | Name = name;
11 | Type = type;
12 |
13 | AddChildren([Name, Type]);
14 | }
15 |
16 | public override void Render(LuauWriter luau)
17 | {
18 | luau.Write("type ");
19 | Name.Render(luau);
20 | luau.Write(" = ");
21 | Type.Render(luau);
22 | luau.WriteLine();
23 | }
24 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/TypeCast.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class TypeCast : Expression
4 | {
5 | public Expression Expression { get; }
6 | public TypeRef Type { get; }
7 |
8 | public TypeCast(Expression expression, TypeRef type)
9 | {
10 | Expression = expression;
11 | Type = type;
12 | AddChildren([Expression, Type]);
13 | }
14 |
15 | public override void Render(LuauWriter luau) => luau.WriteTypeCast(Expression, Type);
16 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/TypeOfCall.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public sealed class TypeOfCall : TypeRef
4 | {
5 | public TypeOfCall(Expression expression)
6 | : base("")
7 | {
8 | Expression = expression;
9 | AddChild(Expression);
10 | }
11 |
12 | public Expression Expression { get; }
13 |
14 | public override void Render(LuauWriter luau)
15 | {
16 | luau.Write("typeof(");
17 | Expression.Render(luau);
18 | luau.Write(")");
19 | }
20 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/TypeRef.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class TypeRef(string path, bool rawPath = false) : Name
4 | {
5 | public string Path { get; set; } = (rawPath ? path : AstUtility.CreateTypeRef(path)!.Path).Trim();
6 |
7 | public override void Render(LuauWriter luau) => luau.Write(Path);
8 |
9 | public override string ToString() => Path;
10 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/UnaryOperator.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class UnaryOperator : Expression
4 | {
5 | public string Operator { get; }
6 | public Expression Operand { get; }
7 |
8 | public UnaryOperator(string @operator, Expression operand)
9 | {
10 | Operator = @operator;
11 | Operand = operand;
12 | AddChild(Operand);
13 | }
14 |
15 | public override void Render(LuauWriter luau)
16 | {
17 | luau.Write(Operator);
18 | Operand.Render(luau);
19 | }
20 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/Variable.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public sealed class Variable : BaseVariable
4 | {
5 | public Variable(IdentifierName name, bool isLocal, Expression? initializer = null, TypeRef? type = null)
6 | : base(isLocal, type)
7 | {
8 | Name = name;
9 | Initializer = initializer;
10 |
11 | AddChild(Name);
12 | if (Initializer != null) AddChild(Initializer);
13 | }
14 |
15 | public IdentifierName Name { get; }
16 | public Expression? Initializer { get; }
17 |
18 | public override void Render(LuauWriter luau) => luau.WriteVariable([Name], IsLocal, Initializer != null ? [Initializer] : [], Type);
19 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/VariableList.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class VariableList : Statement
4 | {
5 | public List Variables { get; }
6 |
7 | public VariableList(List variables)
8 | {
9 | Variables = variables;
10 | AddChildren(Variables);
11 | }
12 |
13 | public override void Render(LuauWriter luau) => luau.WriteNodes(Variables);
14 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/AST/While.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class While : Statement
4 | {
5 | public Expression Condition { get; }
6 | public Statement Body { get; }
7 |
8 | public While(Expression condition, Statement body)
9 | {
10 | Condition = condition;
11 | Body = body;
12 | AddChildren([Condition, Body]);
13 | }
14 |
15 | public override void Render(LuauWriter luau)
16 | {
17 | luau.Write("while ");
18 | Condition.Render(luau);
19 | luau.WriteLine(" do");
20 | luau.PushIndent();
21 | Body.Render(luau);
22 | luau.PopIndent();
23 | luau.WriteLine("end");
24 | }
25 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/BaseWriter.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace RobloxCS.Luau;
4 |
5 | public class BaseWriter
6 | {
7 | public const int IndentSize = 2;
8 | private readonly StringBuilder _output = new();
9 | private int _indent;
10 |
11 | public override string ToString() => _output.ToString();
12 | public void PushIndent() => _indent++;
13 | public void PopIndent() => _indent--;
14 | public void WriteLine() => Write('\n');
15 | public void WriteLine(char text) => WriteLine(text.ToString());
16 |
17 | public void WriteLine(string text)
18 | {
19 | if (string.IsNullOrEmpty(text))
20 | {
21 | _output.AppendLine();
22 |
23 | return;
24 | }
25 |
26 | WriteIndent();
27 | _output.Append(text);
28 | _output.Append('\n');
29 | }
30 |
31 | public void Write(char text) => Write(text.ToString());
32 |
33 | public void Write(string text)
34 | {
35 | WriteIndent();
36 | _output.Append(text);
37 | }
38 |
39 | public void Remove(int amount) => _output.Remove(_output.Length - 1, amount);
40 |
41 | private void WriteIndent() => _output.Append(WasLastCharacter('\n') ? string.Concat(Enumerable.Repeat(" ", IndentSize * _indent)) : "");
42 |
43 | private bool WasLastCharacter(char character)
44 | {
45 | if (_output.Length == 0) return false;
46 |
47 | return _output[^1] == character;
48 | }
49 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/FileCompilation.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using RobloxCS.Shared;
3 |
4 | namespace RobloxCS.Luau;
5 |
6 | public class FileCompilation
7 | {
8 | public required SyntaxTree Tree { get; set; }
9 | public required ConfigData Config { get; init; }
10 | public required RojoProject? RojoProject { get; init; }
11 | public Prerequisites Prerequisites { get; } = new();
12 | public OccupiedIdentifiersStack OccupiedIdentifiers { get; } = new();
13 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/LuauWriter.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Luau;
2 |
3 | public class LuauWriter : BaseWriter
4 | {
5 | public string Render(AST ast)
6 | {
7 | ast.Render(this);
8 |
9 | return ToString();
10 | }
11 |
12 | public void WriteNodesCommaSeparated(IEnumerable nodes)
13 | where TNode : Node
14 | {
15 | var nodesList = nodes.ToList();
16 | var index = 0;
17 | foreach (var node in nodesList)
18 | {
19 | node.Render(this);
20 | if (index++ != nodesList.Count - 1) Write(", ");
21 | }
22 | }
23 |
24 | public void WriteNodes(List nodes)
25 | where TNode : Node
26 | {
27 | foreach (var node in nodes) node.Render(this);
28 | }
29 |
30 | public void WriteRequire(string requirePath) => WriteLine($"require({requirePath})");
31 |
32 | public void WriteFunction(Name? name,
33 | bool isLocal,
34 | ParameterList parameterList,
35 | TypeRef? returnType = null,
36 | Block? body = null,
37 | List? attributeLists = null,
38 | List? typeParameters = null,
39 | bool inlineAttributes = false,
40 | bool createNewline = true)
41 | {
42 | foreach (var attributeList in attributeLists ?? [])
43 | {
44 | attributeList.Inline = inlineAttributes;
45 | attributeList.Render(this);
46 | }
47 |
48 | if (isLocal) Write("local ");
49 |
50 | Write("function");
51 | if (name != null)
52 | {
53 | Write(' ');
54 | name.Render(this);
55 | }
56 |
57 | if (typeParameters != null)
58 | {
59 | Write('<');
60 | WriteNodes(typeParameters);
61 | Write('>');
62 | }
63 |
64 | parameterList.Render(this);
65 | WriteTypeAnnotation(returnType);
66 | WriteLine();
67 | PushIndent();
68 |
69 | body ??= new Block([]);
70 | foreach (var parameter in parameterList.Parameters)
71 | if (parameter.IsVararg)
72 | {
73 | var type = parameter.Type != null ? AstUtility.CreateTypeRef(parameter.Type.Path + "[]") : null;
74 | var value = new TableInitializer([AstUtility.Vararg]);
75 | body.Statements.Insert(0, new Variable(parameter.Name, true, value, type));
76 | }
77 | else if (parameter.Initializer != null)
78 | {
79 | body.Statements.Insert(0, AstUtility.DefaultValueInitializer(parameter.Name, parameter.Initializer));
80 | }
81 |
82 | body.Render(this);
83 |
84 | PopIndent();
85 | if (createNewline)
86 | WriteLine("end");
87 | else
88 | Write("end");
89 | }
90 |
91 | public void WriteAssignment(Expression name, Expression initializer)
92 | {
93 | name.Render(this);
94 | Write(" = ");
95 | initializer.Render(this);
96 | WriteLine();
97 | }
98 |
99 | public void WriteVariable(HashSet names, bool isLocal, List initializers, TypeRef? type = null)
100 | {
101 | if (isLocal) Write("local ");
102 |
103 | WriteNodesCommaSeparated(names);
104 | WriteTypeAnnotation(type);
105 | if (initializers.Count > 0)
106 | {
107 | Write(" = ");
108 | WriteNodesCommaSeparated(initializers);
109 | }
110 |
111 | WriteLine();
112 | }
113 |
114 | public void WriteReturn(Expression? expression = null)
115 | {
116 | Write("return ");
117 | (expression ?? new Literal("nil")).Render(this);
118 | WriteLine();
119 | }
120 |
121 | public void WriteTypeAnnotation(TypeRef? type)
122 | {
123 | if (type == null) return;
124 |
125 | Write(": ");
126 | type.Render(this);
127 | }
128 |
129 | public void WriteTypeCast(Expression expression, TypeRef type)
130 | {
131 | expression.Render(this);
132 | Write(" :: ");
133 | type.Render(this);
134 | }
135 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/OccupiedIdentifiersStack.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using RobloxCS.Luau;
3 |
4 | namespace RobloxCS;
5 |
6 | public class OccupiedIdentifiersStack : Stack>
7 | {
8 | public void Push()
9 | {
10 | List newList = [];
11 | Push(newList);
12 | }
13 |
14 | public string GetDuplicateText(string text)
15 | {
16 | text = text.Replace("@", "");
17 |
18 | var occurrences = CountOccurrences(text) - 1;
19 | var newText = occurrences > 0 ? "_" + occurrences : "";
20 | var halves = text.Split('<'); // generics, poopoo.
21 | var duplicateText = halves.First() + newText + (halves.Length > 1 ? "<" + halves.Last() : "");
22 |
23 | return duplicateText;
24 | }
25 |
26 | public int CountOccurrences(string text) => Peek().Count(identifier => identifier.Text == text);
27 |
28 | public IdentifierName AddIdentifier(SyntaxNode node, string text) => AddIdentifier(node.GetFirstToken(), text);
29 | public IdentifierName AddIdentifier(SyntaxToken token) => AddIdentifier(token, token.Text);
30 |
31 | public IdentifierName AddIdentifier(SyntaxToken token, string text)
32 | {
33 | var verbatim = text.Replace("@", "");
34 |
35 | if (AstUtility.CheckReservedName(token, verbatim)) return null!;
36 |
37 | return AddIdentifier(verbatim);
38 | }
39 |
40 | /// Note: Skips reserved Luau name checks
41 | public IdentifierName AddIdentifier(string text)
42 | {
43 | AddIdentifier(new IdentifierName(text.Replace("@", "")));
44 |
45 | return new IdentifierName(GetDuplicateText(text));
46 | }
47 |
48 | private void AddIdentifier(IdentifierName identifierName) => Peek().Add(identifierName);
49 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/Prerequisites.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Shared;
2 |
3 | namespace RobloxCS.Luau;
4 |
5 | public class Prerequisites
6 | {
7 | private readonly Stack> _statementsStack = [];
8 |
9 | public void Add(Statement statement) => _statementsStack.Peek().Add(statement);
10 | public void AddList(List statements) => _statementsStack.Peek().AddRange(statements);
11 |
12 | public List CaptureOnlyPrereqs(Action callback)
13 | {
14 | PushStatementsStack();
15 | callback();
16 |
17 | return PopStatementsStack();
18 | }
19 |
20 | public (T, List) Capture(Func callback)
21 | {
22 | T? value = default;
23 | var prereqs = CaptureOnlyPrereqs(() => value = callback());
24 |
25 | return (value!, prereqs);
26 | }
27 |
28 | public Expression NoPrereqs(Func callback)
29 | {
30 | Expression? expression = null;
31 | var statements = CaptureOnlyPrereqs(() => expression = callback());
32 | if (statements.Count > 0)
33 | Logger.CompilerError("Assertion of no prereqs failed for " + (expression?.ToString() ?? "expression"));
34 |
35 | return expression!;
36 | }
37 |
38 | private void PushStatementsStack()
39 | {
40 | List statements = [];
41 | _statementsStack.Push(statements);
42 | }
43 |
44 | private List PopStatementsStack()
45 | {
46 | if (!_statementsStack.TryPop(out var popped))
47 | Logger.CompilerError("Failed to pop prereq statements stack");
48 |
49 | return popped!;
50 | }
51 | }
--------------------------------------------------------------------------------
/RobloxCS.Luau/README.md:
--------------------------------------------------------------------------------
1 | # Luau AST
2 |
3 | This is where Luau AST nodes and their text representations reside.
--------------------------------------------------------------------------------
/RobloxCS.Luau/RobloxCS.Luau.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Library
5 | net9.0
6 | RobloxCS.Luau
7 | enable
8 | enable
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/RobloxCS.Luau/SymbolMetadataManager.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace RobloxCS.Luau;
4 |
5 | public class SymbolMetadata
6 | {
7 | public IdentifierName? EventConnectionName { get; set; }
8 | public Dictionary>? MethodOverloads { get; set; }
9 | }
10 |
11 | public static class SymbolMetadataManager
12 | {
13 | private static readonly Dictionary _metadata = [];
14 |
15 | public static SymbolMetadata Get(ISymbol symbol)
16 | {
17 | var metadata = _metadata.GetValueOrDefault(symbol);
18 | if (metadata == null) _metadata.Add(symbol, metadata = new SymbolMetadata());
19 |
20 | return metadata;
21 | }
22 | }
--------------------------------------------------------------------------------
/RobloxCS.Shared/Analysis.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Shared;
2 |
3 | public sealed class AnalysisResult
4 | {
5 | public TypeClassInfo TypeClassInfo { get; } = new();
6 | public MemberClassInfo MemberClassInfo { get; } = new();
7 | public MethodBaseClassInfo MethodBaseClassInfo { get; } = new();
8 | public AssemblyClassInfo AssemblyClassInfo { get; } = new();
9 | public ModuleClassInfo ModuleClassInfo { get; } = new();
10 | public PropertyClassInfo PropertyClassInfo { get; } = new();
11 | public CustomAttributeDataClassInfo CustomAttributeDataClassInfo { get; } = new();
12 | }
13 |
14 | public abstract class BaseClassInfo
15 | {
16 | public HashSet MemberUses { get; } = [];
17 | }
18 |
19 | // temporary or something idk
20 | public sealed class TypeClassInfo : BaseClassInfo;
21 | public sealed class MemberClassInfo : BaseClassInfo;
22 | public sealed class MethodBaseClassInfo : BaseClassInfo;
23 | public sealed class AssemblyClassInfo : BaseClassInfo;
24 | public sealed class ModuleClassInfo : BaseClassInfo;
25 | public sealed class PropertyClassInfo : BaseClassInfo;
26 | public sealed class CustomAttributeDataClassInfo : BaseClassInfo;
--------------------------------------------------------------------------------
/RobloxCS.Shared/Config.cs:
--------------------------------------------------------------------------------
1 | namespace RobloxCS.Shared;
2 |
3 | public sealed class ConfigData
4 | {
5 | public required string SourceFolder { get; init; }
6 | public required string OutputFolder { get; init; }
7 | public required object[] EntryPointArguments { get; init; } = [];
8 | public required string RojoProjectName { get; init; } = "default";
9 |
10 | public bool IsValid() =>
11 | !string.IsNullOrEmpty(SourceFolder)
12 | && !string.IsNullOrEmpty(OutputFolder)
13 | && !string.IsNullOrEmpty(RojoProjectName);
14 | }
--------------------------------------------------------------------------------
/RobloxCS.Shared/ConfigReader.cs:
--------------------------------------------------------------------------------
1 | using YamlDotNet.Serialization;
2 | using YamlDotNet.Serialization.NamingConventions;
3 |
4 | namespace RobloxCS.Shared;
5 |
6 | public static class ConfigReader
7 | {
8 | public static ConfigData UnitTestingConfig { get; } = new()
9 | {
10 | SourceFolder = "test-src",
11 | OutputFolder = "test-dist",
12 | RojoProjectName = "UNIT_TESTING",
13 | EntryPointArguments = []
14 | };
15 |
16 | private const string _fileName = "roblox-cs.yml";
17 |
18 | public static ConfigData Read(string inputDirectory)
19 | {
20 | var configPath = inputDirectory + "/" + _fileName;
21 | ConfigData? config = null;
22 | var ymlContent = "";
23 |
24 | try
25 | {
26 | ymlContent = File.ReadAllText(configPath);
27 | }
28 | catch (Exception e)
29 | {
30 | FailToRead(e.Message);
31 | }
32 |
33 | var deserializer = new DeserializerBuilder()
34 | .WithNamingConvention(PascalCaseNamingConvention.Instance)
35 | .WithAttemptingUnquotedStringTypeDeserialization()
36 | .WithDuplicateKeyChecking()
37 | .Build();
38 |
39 | try
40 | {
41 | config = deserializer.Deserialize(ymlContent);
42 | }
43 | catch (Exception e)
44 | {
45 | FailToRead(e.ToString());
46 | }
47 |
48 | if (config == null || !config.IsValid())
49 | FailToRead($"Invalid {_fileName}! Make sure it has all required fields.");
50 |
51 | return config!;
52 | }
53 |
54 | private static void FailToRead(string message) =>
55 | throw Logger.Error($"Failed to read {_fileName}!\n{message}");
56 | }
--------------------------------------------------------------------------------
/RobloxCS.Shared/Constants.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 |
3 | namespace RobloxCS.Shared;
4 |
5 | public static class Constants
6 | {
7 | public const string IncludeFolderName = "Include";
8 |
9 | public static readonly HashSet UNSUPPORTED_BITWISE_TYPES = ["UInt128", "ulong", "long", "Int128"];
10 |
11 | public static readonly HashSet LENGTH_READABLE_TYPES = ["String", "string", "Array"];
12 |
13 | public static readonly HashSet MEMBER_PARENT_SYNTAXES =
14 | [
15 | SyntaxKind.NamespaceDeclaration, SyntaxKind.ClassDeclaration, SyntaxKind.InterfaceDeclaration, SyntaxKind.StructDeclaration
16 | ];
17 |
18 | public static readonly HashSet GLOBAL_LIBRARIES =
19 | [
20 | "task",
21 | "math",
22 | "table",
23 | "os",
24 | "buffer",
25 | "coroutine",
26 | "utf8",
27 | "debug"
28 | ];
29 |
30 | public static readonly HashSet METAMETHODS =
31 | [
32 | "__tostring",
33 | "__add",
34 | "__sub",
35 | "__mul",
36 | "__div",
37 | "__idiv",
38 | "__mod",
39 | "__pow",
40 | "__unm",
41 | "__eq",
42 | "__le",
43 | "__lte",
44 | "__len",
45 | "__iter",
46 | "__call",
47 | "__concat",
48 | "__mode",
49 | "__index",
50 | "__newindex",
51 | "__metatable"
52 | ];
53 |
54 | public static readonly HashSet LUAU_KEYWORDS =
55 | [
56 | "local",
57 | "and",
58 | "or",
59 | "if",
60 | "else",
61 | "elseif",
62 | "then",
63 | "do",
64 | "end",
65 | "function",
66 | "for",
67 | "while",
68 | "in",
69 | "export",
70 | "type",
71 | "typeof"
72 | ];
73 |
74 | public static readonly HashSet RESERVED_IDENTIFIERS = ["CS", "next", ..LUAU_KEYWORDS];
75 |
76 | public static readonly HashSet DECIMAL_TYPES = ["float", "double", "Single", "Double"];
77 |
78 | public static readonly HashSet INTEGER_TYPES =
79 | [
80 | "sbyte",
81 | "byte",
82 | "short",
83 | "ushort",
84 | "int",
85 | "uint",
86 | "long",
87 | "ulong",
88 | "SByte",
89 | "Byte",
90 | "Int16",
91 | "Int32",
92 | "Int64",
93 | "Int128",
94 | "UInt16",
95 | "UInt32",
96 | "UInt64",
97 | "UInt128"
98 | ];
99 | }
--------------------------------------------------------------------------------
/RobloxCS.Shared/FileUtility.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Text.RegularExpressions;
3 | using System.Xml.Linq;
4 | using Microsoft.CodeAnalysis;
5 | using RobloxCS.Shared;
6 | using Path = System.IO.Path;
7 |
8 | namespace RobloxCS;
9 |
10 | public static class FileUtility
11 | {
12 | private const string _runtimeAssemblyName = "Roblox";
13 |
14 | public static string? GetRbxcsDirectory()
15 | {
16 | var directoryName = Path.GetDirectoryName(GetAssemblyDirectory());
17 |
18 | return directoryName == null ? null : FixPathSeparator(directoryName);
19 | }
20 |
21 | public static List GetCompilationReferences()
22 | {
23 | var runtimeLibAssemblyPath = string.Join('/', GetAssemblyDirectory(), _runtimeAssemblyName + ".dll");
24 | if (!File.Exists(runtimeLibAssemblyPath))
25 | {
26 | var directoryName = Path.GetDirectoryName(runtimeLibAssemblyPath);
27 | var location = directoryName == null
28 | ? "(could not find assembly directory)"
29 | : FixPathSeparator(directoryName);
30 |
31 | Logger.Error($"Failed to find {_runtimeAssemblyName}.dll in {location}");
32 | }
33 |
34 | return
35 | [
36 | MetadataReference.CreateFromFile(runtimeLibAssemblyPath),
37 | ..GetCoreLibReferences()
38 | ];
39 | }
40 |
41 | private static HashSet GetCoreLibReferences()
42 | {
43 | var coreLib = typeof(object).Assembly.Location;
44 | HashSet coreDlls = ["System.Runtime.dll", "System.Core.dll", "System.Collections.dll"];
45 | HashSet references = [MetadataReference.CreateFromFile(coreLib)];
46 |
47 | foreach (var dllPath in coreDlls.Select(coreDll => Path.Combine(Path.GetDirectoryName(coreLib)!, coreDll)))
48 | references.Add(MetadataReference.CreateFromFile(dllPath));
49 |
50 | references.Add(MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location));
51 | references.Add(MetadataReference.CreateFromFile(typeof(List<>).Assembly.Location));
52 |
53 | // TODO: add references to installed packages
54 | return references;
55 | }
56 |
57 | private static string FixPathSeparator(string path)
58 | {
59 | var cleanedPath = Path.TrimEndingDirectorySeparator(path)
60 | .Replace(@"\\", "/")
61 | .Replace('\\', '/')
62 | .Replace("//", "/");
63 |
64 | return Regex.Replace(cleanedPath, @"(? Log(message, ConsoleColor.Green, "OK");
22 |
23 | public static void Info(string message) => Log(message, ConsoleColor.Cyan, "INFO");
24 |
25 | public static CleanExitException Error(string message)
26 | {
27 | Log(message, ConsoleColor.Red, "ERROR");
28 | return new CleanExitException(message);
29 | }
30 |
31 | public static CleanExitException CompilerError(string message, SyntaxNode node) =>
32 | CodegenError(node, message + _compilerError);
33 |
34 | public static CleanExitException CompilerError(string message, SyntaxToken token) =>
35 | CodegenError(token, message + _compilerError);
36 |
37 | public static CleanExitException CompilerError(string message) => Error(message + _compilerError);
38 |
39 | public static CleanExitException CodegenError(SyntaxToken token, string message) =>
40 | Error($"{message}\n\t- {FormatLocation(token.GetLocation().GetLineSpan())}");
41 |
42 | public static void CodegenWarning(SyntaxToken token, string message)
43 | {
44 | var lineSpan = token.GetLocation().GetLineSpan();
45 | Warn($"{message}\n\t- {FormatLocation(lineSpan)}");
46 | }
47 |
48 | public static CleanExitException UnsupportedError(SyntaxNode node, string subject, bool useIs = false, bool useYet = true) =>
49 | UnsupportedError(node.GetFirstToken(), subject, useIs, useYet);
50 |
51 | public static CleanExitException UnsupportedError(SyntaxToken token, string subject, bool useIs = false, bool useYet = true) =>
52 | CodegenError(token, $"{subject} {(useIs ? "is" : "are")} not {(useYet ? "yet " : "")}supported, sorry!");
53 |
54 | public static CleanExitException CodegenError(SyntaxNode node, string message) =>
55 | CodegenError(node.GetFirstToken(), message);
56 |
57 | public static void CodegenWarning(SyntaxNode node, string message) =>
58 | CodegenWarning(node.GetFirstToken(), message);
59 |
60 | public static void HandleDiagnostic(Diagnostic diagnostic)
61 | {
62 | HashSet ignoredCodes = ["CS7022", "CS0017" /* more than one entry point */];
63 |
64 | if (ignoredCodes.Contains(diagnostic.Id)) return;
65 |
66 | var lineSpan = diagnostic.Location.GetLineSpan();
67 | var diagnosticMessage = $"{diagnostic.Id}: {diagnostic.GetMessage()}";
68 | var location = $"\n\t- {FormatLocation(lineSpan)}";
69 | switch (diagnostic.Severity)
70 | {
71 | case DiagnosticSeverity.Error:
72 | {
73 | Error(diagnosticMessage + location);
74 | break;
75 | }
76 | case DiagnosticSeverity.Warning:
77 | {
78 | if (diagnostic.IsWarningAsError)
79 | Error(diagnosticMessage + location);
80 | else
81 | Warn(diagnosticMessage + location);
82 |
83 | break;
84 | }
85 | case DiagnosticSeverity.Info:
86 | {
87 | Info(diagnosticMessage);
88 | break;
89 | }
90 | }
91 | }
92 |
93 | public static void Warn(string message) => Log(message, ConsoleColor.Yellow, "WARN");
94 |
95 | public static void Debug(string message) => Log(message, ConsoleColor.Magenta, "DEBUG");
96 |
97 | private static void Log(string message, ConsoleColor color, string level)
98 | {
99 | var originalColor = Console.ForegroundColor;
100 | Console.ForegroundColor = color;
101 | Console.WriteLine($"[{level}] {message}");
102 | Console.ForegroundColor = originalColor;
103 | }
104 |
105 | private static string FormatLocation(FileLinePositionSpan lineSpan) =>
106 | $"{(lineSpan.Path == "" ? "" : lineSpan.Path)}:{lineSpan.StartLinePosition.Line + 1}:{lineSpan.StartLinePosition.Character + 1}";
107 | }
--------------------------------------------------------------------------------
/RobloxCS.Shared/RobloxCS.Shared.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | RobloxCS.Shared
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/RobloxCS.Shared/RojoReader.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace RobloxCS.Shared;
6 |
7 | public sealed class RojoProject
8 | {
9 | [JsonPropertyName("name")] public string Name { get; init; }
10 |
11 | [JsonPropertyName("tree")] public InstanceDescription Tree { get; init; }
12 |
13 | [JsonPropertyName("servePort")] public int ServePort { get; init; } = 34872;
14 |
15 | [JsonPropertyName("servePlaceIds")] public List ServePlaceIDs { get; init; } = [];
16 |
17 | [JsonPropertyName("placeId")] public string? PlaceId { get; init; }
18 |
19 | [JsonPropertyName("gameId")] public string? GameId { get; init; }
20 |
21 | [JsonPropertyName("serveAddress")] public string? ServeAddress { get; init; }
22 |
23 | [JsonPropertyName("globIgnorePaths")] public List GlobIgnorePaths { get; init; } = [];
24 |
25 | [JsonPropertyName("emitLegacyScripts")]
26 | public bool EmitLegacyScripts { get; init; } = true;
27 |
28 | public bool IsValid() => !string.IsNullOrEmpty(Name) && Tree != null;
29 | }
30 |
31 | public sealed class InstanceDescription
32 | {
33 | [JsonPropertyName("$className")]
34 | public string? ClassName { get; init; }
35 |
36 | [JsonPropertyName("$path")]
37 | public string? Path { get; init; }
38 |
39 | [JsonPropertyName("$properties")]
40 | public Dictionary? Properties { get; init; }
41 |
42 | [JsonPropertyName("$ignoreUnknownInstances")]
43 | public bool IgnoreUnknownInstances { get; init; } = true;
44 | public Dictionary Instances { get; init; } = [];
45 |
46 | [JsonExtensionData]
47 | public IDictionary AdditionalData { get; init; } = new Dictionary();
48 |
49 | public void OnDeserialized()
50 | {
51 | foreach (var kvp in AdditionalData)
52 | {
53 | var childInstance = kvp.Value.Deserialize()!;
54 | Instances[kvp.Key] = childInstance;
55 | childInstance.OnDeserialized();
56 | }
57 | }
58 | }
59 |
60 | public static class RojoReader
61 | {
62 | public static RojoProject Read(string projectPath)
63 | {
64 | var jsonContent = "";
65 | RojoProject? project = null;
66 |
67 | try
68 | {
69 | jsonContent = File.ReadAllText(projectPath);
70 | }
71 | catch (Exception e)
72 | {
73 | FailToRead(projectPath, e.Message);
74 | }
75 |
76 | try
77 | {
78 | project = JsonSerializer.Deserialize(jsonContent);
79 | }
80 | catch (Exception e)
81 | {
82 | FailToRead(projectPath, e.ToString());
83 | }
84 |
85 | if (project == null || !project.IsValid())
86 | FailToRead(projectPath, "Invalid Rojo project! Make sure it has all required fields ('name' and 'tree').");
87 |
88 | UpdateChildInstances(project!.Tree);
89 | return project!;
90 | }
91 |
92 | public static RojoProject? ReadFromDirectory(string inputDirectory, string projectName)
93 | {
94 | if (projectName == "UNIT_TESTING") return null;
95 |
96 | var path = FindProjectPath(inputDirectory, projectName);
97 | if (path == null)
98 | throw Logger.Error($"Failed to find Rojo project file \"{projectName}.project.json\"!");
99 |
100 | return Read(path);
101 | }
102 |
103 | public static string? ResolveInstancePath(RojoProject project, string filePath)
104 | {
105 | var path = TraverseInstanceTree(project.Tree, StandardUtility.FixPathSeparator(filePath));
106 | return path == null ? null : FormatInstancePath(StandardUtility.FixPathSeparator(path));
107 | }
108 |
109 | private static string? FindProjectPath(string directoryPath, string projectName) =>
110 | Directory.GetFiles(directoryPath)
111 | .FirstOrDefault(file => Path.GetFileName(file) == $"{projectName}.project.json");
112 |
113 | private static string? TraverseInstanceTree(InstanceDescription instance, string filePath)
114 | {
115 | var instancePath = instance.Path != null ? StandardUtility.FixPathSeparator(instance.Path) : null;
116 | if (instancePath != null && filePath.StartsWith(instancePath))
117 | {
118 | var remainingPath = filePath[(instancePath.Length + 1)..]; // +1 to omit '/'
119 | return Path.ChangeExtension(remainingPath, null);
120 | }
121 |
122 | foreach (var (leftName, value) in instance.Instances)
123 | {
124 | var result = TraverseInstanceTree(value, filePath);
125 | if (result == null) continue;
126 |
127 | return $"{leftName}/{result}";
128 | }
129 |
130 | return null;
131 | }
132 |
133 | private static string FormatInstancePath(string path)
134 | {
135 | var segments = path.Split('/');
136 | var formattedPath = new StringBuilder();
137 |
138 | foreach (var segment in segments)
139 | {
140 | if (segment == segments.First())
141 | {
142 | formattedPath.Append(segment);
143 | }
144 | else
145 | {
146 | formattedPath.Append(formattedPath.Length > 0 ? "[\"" : "");
147 | formattedPath.Append(segment);
148 | formattedPath.Append("\"]");
149 | }
150 | }
151 |
152 | return formattedPath.ToString();
153 | }
154 |
155 | private static void UpdateChildInstances(InstanceDescription instance)
156 | {
157 | instance.OnDeserialized();
158 | foreach (var childInstance in instance.Instances.Values)
159 | UpdateChildInstances(childInstance);
160 | }
161 |
162 | private static void FailToRead(string configPath, string message) =>
163 | throw Logger.Error($"Failed to read {configPath}!\nReason: {message}");
164 | }
--------------------------------------------------------------------------------
/RobloxCS.Shared/StandardUtility.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Text.RegularExpressions;
3 | using Microsoft.CodeAnalysis;
4 | using Microsoft.CodeAnalysis.CSharp;
5 | using Microsoft.CodeAnalysis.CSharp.Syntax;
6 | using static RobloxCS.Shared.Constants;
7 |
8 | namespace RobloxCS.Shared;
9 |
10 | public static class StandardUtility
11 | {
12 | public static Type GetRuntimeType(SemanticModel semanticModel, SyntaxNode node, ITypeSymbol typeSymbol)
13 | {
14 | var fullyQualifiedName = GetFullSymbolName(typeSymbol);
15 |
16 | Type? type = null;
17 | var assemblyContainsError = false;
18 | using (var memoryStream = new MemoryStream())
19 | {
20 | // var emitResult =
21 | semanticModel.Compilation.Emit(memoryStream);
22 |
23 | // var errors = emitResult.Diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToList();
24 | // if (errors.Count > 0)
25 | // Logger.CodegenWarning(node, $"[GetRuntimeType()] Semantic model compilation had errors:\n{string.Join('\n', errors.Select(e => e.GetMessage()))}");
26 |
27 | memoryStream.Seek(0, SeekOrigin.Begin);
28 | Assembly? assembly = null;
29 | try
30 | {
31 | assembly = Assembly.Load(memoryStream.ToArray());
32 | }
33 | catch (Exception e)
34 | {
35 | assemblyContainsError = true;
36 |
37 | // temporarily commented
38 | // throw Logger.CodegenError(node, $"Failed to resolve runtime type '{fullyQualifiedName}' because the assembly could not be loaded: {e.Message}");
39 | }
40 |
41 | if (assembly != null)
42 | type = assembly.GetType(fullyQualifiedName); // get the type from the loaded assembly
43 | }
44 |
45 | type ??= Type.GetType(fullyQualifiedName);
46 | if (type == null && !assemblyContainsError)
47 | throw Logger.CodegenError(node, $"[GetRuntimeType()]: Unable to resolve type '{fullyQualifiedName}'.");
48 |
49 | return type!;
50 | }
51 |
52 | public static string GetFullSymbolName(ISymbol symbol)
53 | {
54 | var containerName = symbol.ContainingNamespace != null || symbol.ContainingType != null
55 | ? GetFullSymbolName(symbol.ContainingNamespace ?? (ISymbol)symbol.ContainingType)
56 | : null;
57 |
58 | return (!string.IsNullOrEmpty(containerName) ? containerName + "." : "") + symbol.Name;
59 | }
60 |
61 | public static bool DoesTypeInheritFrom(ITypeSymbol? derived, string typeName)
62 | {
63 | if (derived == null)
64 | return false;
65 |
66 | return derived.Name == typeName
67 | || derived.BaseType != null
68 | && DoesTypeInheritFrom(derived.BaseType, typeName);
69 | }
70 |
71 | public static bool DoesTypeInheritFrom(ITypeSymbol derived, ITypeSymbol baseType)
72 | {
73 | var current = derived;
74 | while (current != null)
75 | {
76 | if (SymbolEqualityComparer.Default.Equals(current, baseType)) return true;
77 |
78 | current = current.BaseType;
79 | }
80 |
81 | return false;
82 | }
83 |
84 | public static string GetDefaultValueForType(string typeName)
85 | {
86 | if (INTEGER_TYPES.Contains(typeName) || DECIMAL_TYPES.Contains(typeName)) return "0";
87 |
88 | return typeName switch
89 | {
90 | "char" or "Char" or "string" or "String" => "\"\"",
91 | "bool" or "Boolean" => "false",
92 | _ => "nil"
93 | };
94 | }
95 |
96 | public static ISymbol? FindMember(INamespaceSymbol namespaceSymbol, string memberName)
97 | {
98 | var member = namespaceSymbol.GetMembers().FirstOrDefault(member => member?.Name == memberName, null);
99 | if (member == null && namespaceSymbol.ContainingNamespace != null) member = FindMember(namespaceSymbol.ContainingNamespace, memberName);
100 |
101 | return member;
102 | }
103 |
104 | public static ISymbol? FindMemberDeep(INamedTypeSymbol namedTypeSymbol, string memberName)
105 | {
106 | var member = namedTypeSymbol.GetMembers().FirstOrDefault(member => member.Name == memberName);
107 | if (namedTypeSymbol.BaseType != null && member == null) return FindMemberDeep(namedTypeSymbol.BaseType, memberName);
108 |
109 | return member;
110 | }
111 |
112 | public static string FixPathSeparator(string path)
113 | {
114 | path = Path.TrimEndingDirectorySeparator(path)
115 | .Replace(@"\\", "/")
116 | .Replace('\\', '/')
117 | .Replace("//", "/");
118 |
119 | return Regex.Replace(path, @"(? nil";
154 | }
155 |
156 | if (csharpType.StartsWith("Func<"))
157 | {
158 | var typeArgs = ExtractTypeArguments(csharpType).ConvertAll(GetMappedType);
159 | var returnType = typeArgs.Last();
160 | typeArgs = typeArgs.SkipLast(1).ToList();
161 |
162 | return $"({string.Join(", ", typeArgs)}) -> {returnType}";
163 | }
164 |
165 | if (csharpType.StartsWith("Dictionary<"))
166 | {
167 | var typeArgs = ExtractTypeArguments(csharpType).ConvertAll(GetMappedType);
168 | var keyType = typeArgs.First();
169 | var valueType = typeArgs.Last();
170 |
171 | return $"{{ [{keyType}]: {valueType} }}";
172 | }
173 |
174 | if (csharpType.StartsWith("IEnumerator<"))
175 | {
176 | var elementType = GetMappedType(ExtractTypeArguments(csharpType).First());
177 | return $"CS.IEnumerator<{elementType}>";
178 | }
179 |
180 | if (csharpType.StartsWith("HashSet<"))
181 | {
182 | var elementType = GetMappedType(ExtractTypeArguments(csharpType).First());
183 | return $"{{ [{elementType}]: boolean }}";
184 | }
185 |
186 | if (csharpType.StartsWith("Roblox.Enum"))
187 | return GetMappedType(csharpType.Replace("Roblox.Enum", "Enum"));
188 |
189 | return csharpType switch
190 | {
191 | "Object" or "object" or "dynamic" => "any",
192 | "void" or "Void" => "()",
193 | "null" => "nil",
194 | "char" or "Char" or "String" => "string",
195 | "Boolean" or "bool" => "boolean",
196 | "System.Index" or "Index" => "number",
197 | "Roblox.Buffer" or "Buffer" => "buffer",
198 | "System.Type" or "Type" => "any", // "CS.Type",
199 | _ => INTEGER_TYPES.Contains(csharpType) || DECIMAL_TYPES.Contains(csharpType)
200 | ? "number"
201 | : csharpType
202 | };
203 | }
204 |
205 | public static string? GetBit32MethodName(string bitOp) =>
206 | bitOp switch
207 | {
208 | "&=" or "&" => "band",
209 | "|=" or "|" => "bor",
210 | "^=" or "^" => "bxor",
211 | ">>=" or ">>" => "rshift",
212 | ">>>=" or ">>>" => "arshift",
213 | "<<=" or "<<" => "lshift",
214 | "~" => "bnot",
215 | _ => null
216 | };
217 |
218 | public static string GetMappedOperator(string op) =>
219 | op switch
220 | {
221 | "++" => "+=",
222 | "--" => "-=",
223 | "!" => "not ",
224 | "!=" => "~=",
225 | "&&" => "and",
226 | "||" => "or",
227 | _ => op
228 | };
229 |
230 | public static bool IsFromSystemNamespace(ISymbol? typeSymbol)
231 | {
232 | if (typeSymbol is not { ContainingNamespace: not null }) return false;
233 |
234 | return typeSymbol.ContainingNamespace.Name == "System" || IsFromSystemNamespace(typeSymbol.ContainingNamespace);
235 | }
236 |
237 | public static List ExtractTypeArguments(string input)
238 | {
239 | var match = Regex.Match(input, "<(?(?:[^<>]+|<(?)|>(?<-open>))*)>");
240 |
241 | if (!match.Success) return [];
242 |
243 | var argumentsRaw = match.Groups[1].Value;
244 | var arguments = SplitGenericArguments(argumentsRaw);
245 |
246 | return arguments.Select(arg => arg.Trim()).ToList();
247 | }
248 |
249 | private static List SplitGenericArguments(string input)
250 | {
251 | var args = new List();
252 | var depth = 0;
253 | var lastSplit = 0;
254 |
255 | for (var i = 0; i < input.Length; i++)
256 | {
257 | var c = input[i];
258 | switch (c)
259 | {
260 | case '<':
261 | depth++;
262 |
263 | break;
264 | case '>':
265 | depth--;
266 |
267 | break;
268 | case ',' when depth == 0:
269 | args.Add(input.Substring(lastSplit, i - lastSplit));
270 | lastSplit = i + 1;
271 |
272 | break;
273 | }
274 | }
275 |
276 | args.Add(input[lastSplit..]);
277 |
278 | return args;
279 | }
280 |
281 | public static bool IsGlobal(SyntaxNode node) => node.Parent.IsKind(SyntaxKind.GlobalStatement) || node.Parent.IsKind(SyntaxKind.CompilationUnit);
282 |
283 | public static NameSyntax GetNameNode(List pieces)
284 | {
285 | if (pieces.Count <= 1) return SyntaxFactory.IdentifierName(pieces.FirstOrDefault() ?? "");
286 |
287 | var left = GetNameNode(pieces.SkipLast(1).ToList());
288 | var right = SyntaxFactory.IdentifierName(pieces.Last());
289 |
290 | return SyntaxFactory.QualifiedName(left, right);
291 | }
292 |
293 | public static List GetNamesFromNode(SyntaxNode? node, bool noGenerics = false)
294 | {
295 | if (node is BaseExpressionSyntax) return [""];
296 |
297 | List names = [];
298 |
299 | if (node == null) return names;
300 |
301 | List addGenerics(List currentNames)
302 | {
303 | var typeParametersProperty = node.GetType().GetProperty("TypeParameterList");
304 | var typeParametersValue = typeParametersProperty?.GetValue(node);
305 |
306 | if (typeParametersProperty != null && typeParametersValue is TypeParameterListSyntax typeParameterList)
307 | return currentNames
308 | .Append('<'
309 | + string.Join(", ", typeParameterList.Parameters.Select(p => GetNamesFromNode(p).First()))
310 | + '>')
311 | .ToList();
312 |
313 | return currentNames;
314 | }
315 |
316 | var nameProperty = node.GetType().GetProperty("Name");
317 | var nameValue = nameProperty?.GetValue(node);
318 |
319 | if (nameProperty != null && nameValue is NameSyntax nameNode) return GetNamesFromNode(nameNode);
320 |
321 | var identifierProperty = node.GetType().GetProperty("Identifier");
322 | var identifierValue = identifierProperty?.GetValue(node);
323 | if (identifierProperty != null && identifierValue is SyntaxToken token)
324 | {
325 | names.Add(token.ValueText.Trim());
326 |
327 | return noGenerics ? names : addGenerics(names);
328 | }
329 |
330 | var childNodes = node.ChildNodes().ToList();
331 | var qualifiedNameNodes = (node is QualifiedNameSyntax qualifiedName
332 | ? [qualifiedName]
333 | : childNodes.OfType()).ToList();
334 |
335 | var simpleNameNodes = (node is SimpleNameSyntax simpleName
336 | ? [simpleName]
337 | : childNodes.OfType()).ToList();
338 |
339 | if (simpleNameNodes.Count <= 1)
340 | foreach (var qualifiedNameNode in qualifiedNameNodes)
341 | {
342 | names.AddRange(GetNamesFromNode(qualifiedNameNode.Left).Select(name => name.Trim()));
343 | names.AddRange(GetNamesFromNode(qualifiedNameNode.Right).Select(name => name.Trim()));
344 | }
345 |
346 | if (qualifiedNameNodes.Count <= 1) names.AddRange(simpleNameNodes.Select(simpleNameNode => simpleNameNode.ToString().Trim()));
347 |
348 | return noGenerics ? names : addGenerics(names);
349 | }
350 |
351 | public class KeyValuePairEqualityComparer : IEqualityComparer>
352 | {
353 | private readonly IEqualityComparer _keyComparer;
354 | private readonly IEqualityComparer _valueComparer;
355 |
356 | public KeyValuePairEqualityComparer(IEqualityComparer keyComparer = null!,
357 | IEqualityComparer valueComparer = null!)
358 | {
359 | _keyComparer = keyComparer ?? EqualityComparer.Default;
360 | _valueComparer = valueComparer ?? EqualityComparer.Default;
361 | }
362 |
363 | public bool Equals(KeyValuePair x, KeyValuePair y) =>
364 | _keyComparer.Equals(x.Key, y.Key) && _valueComparer.Equals(x.Value, y.Value);
365 |
366 | public int GetHashCode(KeyValuePair obj)
367 | {
368 | var hashKey = _keyComparer.GetHashCode(obj.Key!);
369 | var hashValue = _valueComparer.GetHashCode(obj.Value!);
370 |
371 | return hashKey ^ hashValue;
372 | }
373 | }
374 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/.lune/RuntimeLibTest.luau:
--------------------------------------------------------------------------------
1 | local CS = require("../../RobloxCS/Include/RuntimeLib")
2 |
3 | -- CS.is()
4 | assert(true, CS.is("abc", "string"))
5 | assert(true, CS.is(123, "number"))
6 | assert(true, CS.is(false, "boolean"))
7 | assert(true, CS.is(nil, "nil"))
8 | assert(true, CS.is(function() end, "function"))
9 |
10 | local MyClass: CS.Class = { __className = "MyClass" }
11 | local myClass = { __className = "MyClass" }
12 | assert(true, CS.is(myClass, MyClass))
13 |
14 | local parent = {}
15 | parent.__index = parent
16 | local child = setmetatable({}, parent)
17 | assert(true, CS.is(child, parent))
18 |
19 | -- CS.defineGlobal() / CS.getGlobal()
20 | CS.defineGlobal("MyValue", 69420)
21 | assert(69420, CS.getGlobal("MyValue"))
22 |
23 |
--------------------------------------------------------------------------------
/RobloxCS.Tests/AstUtilityTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 | using RobloxCS.Luau;
3 | using RobloxCS.Shared;
4 |
5 | namespace RobloxCS.Tests;
6 |
7 | public class AstUtilityTest
8 | {
9 | public AstUtilityTest() => Logger.Exit = false;
10 |
11 | [Theory]
12 | [InlineData("CS")]
13 | [InlineData("then")]
14 | [InlineData("and")]
15 | [InlineData("end")]
16 | [InlineData("do")]
17 | [InlineData("typeof")]
18 | [InlineData("type")]
19 | [InlineData("export")]
20 | public void ThrowsWithReservedIdentifier(string identifier)
21 | {
22 | Assert.Throws(() => AstUtility.CreateSimpleName(SyntaxFactory.LiteralExpression(SyntaxKind
23 | .NullLiteralExpression),
24 | identifier));
25 | }
26 |
27 | [Fact]
28 | public void AddOne()
29 | {
30 | var expression = AstUtility.AddOne(new IdentifierName("a"));
31 | Assert.IsType(expression);
32 |
33 | var binaryOperator = (BinaryOperator)expression;
34 | Assert.IsType(binaryOperator.Left);
35 | Assert.IsType(binaryOperator.Right);
36 | Assert.Equal("+", binaryOperator.Operator);
37 |
38 | var identifier = (IdentifierName)binaryOperator.Left;
39 | var literal = (Literal)binaryOperator.Right;
40 | Assert.Equal("a", identifier.Text);
41 | Assert.Equal("1", literal.ValueText);
42 | }
43 |
44 | [Fact]
45 | public void AddOne_AddsToLiteralValue()
46 | {
47 | var sum = AstUtility.AddOne(new Literal("1"));
48 | Assert.IsType(sum);
49 |
50 | var literal = (Literal)sum;
51 | Assert.Equal("2", literal.ValueText);
52 | }
53 |
54 | [Fact]
55 | public void SubtractOne()
56 | {
57 | var expression = AstUtility.SubtractOne(new IdentifierName("a"));
58 | Assert.IsType(expression);
59 |
60 | var binaryOperator = (BinaryOperator)expression;
61 | Assert.IsType(binaryOperator.Left);
62 | Assert.IsType(binaryOperator.Right);
63 | Assert.Equal("-", binaryOperator.Operator);
64 |
65 | var identifier = (IdentifierName)binaryOperator.Left;
66 | var literal = (Literal)binaryOperator.Right;
67 | Assert.Equal("a", identifier.Text);
68 | Assert.Equal("1", literal.ValueText);
69 | }
70 |
71 | [Fact]
72 | public void SubtractOne_SubtractsFromLiteralValue()
73 | {
74 | var sum = AstUtility.SubtractOne(new Literal("2"));
75 | Assert.IsType(sum);
76 |
77 | var literal = (Literal)sum;
78 | Assert.Equal("1", literal.ValueText);
79 | }
80 |
81 | [Theory]
82 | [InlineData("var")]
83 | [InlineData(null)]
84 | public void CreateTypeRef_ReturnsNull(string? path) => Assert.Null(AstUtility.CreateTypeRef(path));
85 |
86 | [Theory]
87 | [InlineData("string?", typeof(OptionalType))]
88 | [InlineData("{ number }", typeof(ArrayType))]
89 | [InlineData("{ [string]: number }", typeof(MappedType))]
90 | public void CreateTypeRef_ReturnsCorrectTypeNode(string? path, Type typeNodeType)
91 | {
92 | var typeRef = AstUtility.CreateTypeRef(path);
93 | Assert.NotNull(typeRef);
94 | Assert.IsType(typeNodeType, typeRef);
95 | }
96 |
97 | [Fact]
98 | public void CreateTypeRef_HandlesNestedPatterns()
99 | {
100 | const string rawPath = "{ [string]: { bool } }??";
101 | var typeRef = AstUtility.CreateTypeRef(rawPath);
102 | Assert.NotNull(typeRef);
103 | Assert.IsType(typeRef);
104 |
105 | var optionalType = (OptionalType)typeRef;
106 | Assert.IsType(optionalType.NonNullableType);
107 |
108 | var mappedType = (MappedType)optionalType.NonNullableType;
109 | Assert.Equal("string", mappedType.KeyType.Path);
110 | Assert.IsType(mappedType.ValueType);
111 |
112 | var arrayType = (ArrayType)mappedType.ValueType;
113 | Assert.Equal("boolean", arrayType.ElementType.Path);
114 | }
115 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/Base/Generation.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Luau;
2 | using RobloxCS.Shared;
3 |
4 | namespace RobloxCS.Tests.Base;
5 |
6 | public abstract class Generation
7 | {
8 | protected static AST Generate(string source)
9 | {
10 | var config = ConfigReader.UnitTestingConfig;
11 | var file = TranspilerUtility.ParseAndTransformTree(source.Trim(), new RojoProject(), config);
12 | var compiler = TranspilerUtility.GetCompiler([file.Tree], config);
13 |
14 | return TranspilerUtility.GetLuauAST(file, compiler);
15 | }
16 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/LuauTests.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Reflection;
3 | using System.Runtime.InteropServices;
4 | using Xunit.Abstractions;
5 |
6 | namespace RobloxCS.Tests;
7 |
8 | public class LuauTests(ITestOutputHelper testOutputHelper)
9 | {
10 | private readonly string _cwd = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Assembly.GetExecutingAssembly()
11 | .Location))))!;
12 |
13 | [Theory]
14 | [InlineData("RuntimeLibTest")]
15 | public void LuauTests_Pass(string scriptName)
16 | {
17 | var lunePath = Path.GetFullPath("lune" + (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : ""), _cwd);
18 | var runScriptArguments = $"run {scriptName}";
19 | var process = new Process
20 | {
21 | StartInfo = new ProcessStartInfo
22 | {
23 | FileName = lunePath,
24 | Arguments = runScriptArguments,
25 | RedirectStandardOutput = true,
26 | RedirectStandardError = true,
27 | UseShellExecute = false,
28 | CreateNoWindow = true,
29 | WorkingDirectory = _cwd
30 | }
31 | };
32 |
33 | try
34 | {
35 | process.Start();
36 |
37 | var output = process.StandardOutput.ReadToEnd();
38 | var error = process.StandardError.ReadToEnd();
39 | process.WaitForExit();
40 |
41 | testOutputHelper.WriteLine($"{scriptName}.luau Errors:");
42 | testOutputHelper.WriteLine(error);
43 | Assert.True(string.IsNullOrWhiteSpace(error));
44 | testOutputHelper.WriteLine($"{scriptName}.luau Output:");
45 | testOutputHelper.WriteLine(output);
46 | Assert.True(string.IsNullOrWhiteSpace(output));
47 | Assert.Equal(0, process.ExitCode);
48 | }
49 | finally
50 | {
51 | process.Dispose();
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/MacroTests/ExtraTest.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Luau;
2 |
3 | namespace RobloxCS.Tests.MacroTests;
4 |
5 | public class ExtraTest : Base.Generation
6 | {
7 | [Fact]
8 | public void CallMacros_Discard_IfUnused()
9 | {
10 | var ast = Generate("HashSet set = []; set.Add(69);");
11 | Assert.NotEmpty(ast.Statements);
12 |
13 | var statements = ast.Statements.Skip(2).ToList();
14 | Assert.IsType(statements[2]);
15 |
16 | var variable = (Variable)statements[2];
17 | Assert.Equal("_", variable.Name.ToString());
18 | }
19 |
20 | [Fact]
21 | public void CallMacros_DoNotDiscard_IfUsed()
22 | {
23 | var ast = Generate("HashSet set = []; var abc = set.Add(69);");
24 | Assert.NotEmpty(ast.Statements);
25 |
26 | var statements = ast.Statements.Skip(2).ToList();
27 | Assert.IsType(statements[2]);
28 |
29 | var variableList = (VariableList)statements[2];
30 | Assert.Equal("abc", variableList.Variables.First().Name.ToString());
31 | }
32 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/MacroTests/HashSetMacrosTest.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Luau;
2 | using RobloxCS.Macros;
3 |
4 | namespace RobloxCS.Tests.MacroTests;
5 |
6 | public class HashSetMacrosTest : Base.Generation
7 | {
8 | [Fact]
9 | public void Macros_Add()
10 | {
11 | var ast = Generate("HashSet set = []; set.Add(69);");
12 | Assert.NotEmpty(ast.Statements);
13 |
14 | var statements = ast.Statements.Skip(2).ToList();
15 | Assert.IsType(statements[0]);
16 | Assert.IsType(statements[1]);
17 | Assert.IsType(statements[2]);
18 |
19 | var variable = (Variable)statements[0];
20 | Assert.Equal("_wasAdded", variable.Name.Text);
21 | Assert.IsType(variable.Initializer);
22 |
23 | var binaryOperator = (BinaryOperator)variable.Initializer;
24 | Assert.Equal("==", binaryOperator.Operator);
25 | Assert.IsType(binaryOperator.Left);
26 | Assert.IsType(binaryOperator.Right);
27 |
28 | var assignment = (Assignment)statements[1];
29 | Assert.IsType(assignment.Target);
30 | Assert.IsType(assignment.Value);
31 |
32 | var assignmentValue = (Literal)assignment.Value;
33 | Assert.Equal("true", assignmentValue.ValueText);
34 |
35 | var elementAccess = (ElementAccess)assignment.Target;
36 | Assert.IsType(elementAccess.Expression);
37 | Assert.IsType(elementAccess.Index);
38 | Assert.Equal("set", elementAccess.Expression.ToString());
39 |
40 | var index = (Literal)elementAccess.Index;
41 | Assert.Equal("69", index.ValueText);
42 |
43 | var expression = ((Variable)statements[2]).Initializer;
44 | Assert.NotNull(expression);
45 | Assert.NotNull(expression.ExpandedByMacro);
46 | Assert.Equal(MacroKind.HashSetMethod, expression.ExpandedByMacro);
47 | Assert.IsType(expression);
48 | Assert.Equal("_wasAdded", expression.ToString());
49 | }
50 |
51 | [Fact]
52 | public void Macros_Remove()
53 | {
54 | var ast = Generate("HashSet set = []; set.Remove(69);");
55 | Assert.NotEmpty(ast.Statements);
56 |
57 | var statements = ast.Statements.Skip(2).ToList();
58 | Assert.IsType(statements[0]);
59 | Assert.IsType(statements[1]);
60 | Assert.IsType(statements[2]);
61 |
62 |
63 | var variable = (Variable)statements[0];
64 | Assert.Equal("_wasRemoved", variable.Name.Text);
65 | Assert.IsType(variable.Initializer);
66 |
67 | var binaryOperator = (BinaryOperator)variable.Initializer;
68 | Assert.Equal("~=", binaryOperator.Operator);
69 | Assert.IsType(binaryOperator.Left);
70 | Assert.IsType(binaryOperator.Right);
71 |
72 | var assignment = (Assignment)statements[1];
73 | Assert.IsType(assignment.Target);
74 | Assert.IsType(assignment.Value);
75 |
76 | var typeCast = (TypeCast)assignment.Value;
77 | Assert.IsType(typeCast.Expression);
78 |
79 | var assignmentValue = (Literal)typeCast.Expression;
80 | Assert.Equal("nil", assignmentValue.ValueText);
81 |
82 | var elementAccess = (ElementAccess)assignment.Target;
83 | Assert.IsType(elementAccess.Expression);
84 | Assert.IsType(elementAccess.Index);
85 | Assert.Equal("set", elementAccess.Expression.ToString());
86 |
87 | var index = (Literal)elementAccess.Index;
88 | Assert.Equal("69", index.ValueText);
89 |
90 | var expression = ((Variable)statements[2]).Initializer;
91 | Assert.NotNull(expression);
92 | Assert.NotNull(expression.ExpandedByMacro);
93 | Assert.Equal(MacroKind.HashSetMethod, expression.ExpandedByMacro);
94 | Assert.IsType(expression);
95 | Assert.Equal("_wasRemoved", expression.ToString());
96 | }
97 |
98 | [Fact]
99 | public void Macros_Contains()
100 | {
101 | var ast = Generate("HashSet set = []; set.Contains(69);");
102 | Assert.NotEmpty(ast.Statements);
103 |
104 | var statement = ast.Statements.Skip(2).First();
105 | Assert.IsType(statement);
106 |
107 | var expression = ((Variable)statement).Initializer;
108 | Assert.IsType(expression);
109 |
110 | var elementAccess = (ElementAccess)expression;
111 | Assert.NotNull(elementAccess.ExpandedByMacro);
112 | Assert.Equal(MacroKind.HashSetMethod, elementAccess.ExpandedByMacro);
113 | Assert.IsType(elementAccess.Expression);
114 | Assert.IsType(elementAccess.Index);
115 | Assert.Equal("set", elementAccess.Expression.ToString());
116 |
117 | var index = (Literal)elementAccess.Index;
118 | Assert.Equal("69", index.ValueText);
119 | }
120 |
121 | [Fact]
122 | public void Macros_Clear()
123 | {
124 | var ast = Generate("HashSet set = []; set.Clear();");
125 | Assert.NotEmpty(ast.Statements);
126 |
127 | var statement = ast.Statements.Skip(2).First();
128 | Assert.IsType(statement);
129 |
130 | var expression = ((ExpressionStatement)statement).Expression;
131 | Assert.IsType(expression);
132 |
133 | var call = (Call)expression;
134 | Assert.NotNull(call.ExpandedByMacro);
135 | Assert.Equal(MacroKind.HashSetMethod, call.ExpandedByMacro);
136 | Assert.Single(call.ArgumentList.Arguments);
137 | Assert.IsType(call.Callee);
138 |
139 | var selfArgument = call.ArgumentList.Arguments.First().Expression;
140 | Assert.IsType(selfArgument);
141 | Assert.Equal("set", selfArgument.ToString());
142 |
143 | var memberAccess = (MemberAccess)call.Callee;
144 | Assert.IsType(memberAccess.Expression);
145 | Assert.IsType(memberAccess.Name);
146 | Assert.Equal("table", memberAccess.Expression.ToString());
147 | Assert.Equal("clear", memberAccess.Name.ToString());
148 | }
149 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/MacroTests/ListMacrosTest.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Luau;
2 | using RobloxCS.Macros;
3 |
4 | namespace RobloxCS.Tests.MacroTests;
5 |
6 | public class ListMacrosTest : Base.Generation
7 | {
8 | [Fact]
9 | public void Macros_Add()
10 | {
11 | var ast = Generate("List l = []; l.Add(69);");
12 | Assert.NotEmpty(ast.Statements);
13 |
14 | var statement = ast.Statements.Skip(2).First();
15 | Assert.IsType(statement);
16 |
17 | var expression = ((ExpressionStatement)statement).Expression;
18 | Assert.IsType(expression);
19 |
20 | var call = (Call)expression;
21 | Assert.NotNull(call.ExpandedByMacro);
22 | Assert.Equal(MacroKind.ListMethod, call.ExpandedByMacro);
23 | Assert.Equal(2, call.ArgumentList.Arguments.Count);
24 | Assert.IsType(call.Callee);
25 |
26 | var selfArgument = call.ArgumentList.Arguments.First().Expression;
27 | var elementArgument = call.ArgumentList.Arguments.Last().Expression;
28 | Assert.IsType(selfArgument);
29 | Assert.IsType(elementArgument);
30 | Assert.Equal("l", selfArgument.ToString());
31 |
32 | var element = (Literal)elementArgument;
33 | Assert.Equal("69", element.ValueText);
34 |
35 | var memberAccess = (MemberAccess)call.Callee;
36 | Assert.IsType(memberAccess.Expression);
37 | Assert.IsType(memberAccess.Name);
38 | Assert.Equal("table", memberAccess.Expression.ToString());
39 | Assert.Equal("insert", memberAccess.Name.ToString());
40 | }
41 |
42 | [Fact]
43 | public void Macros_Insert()
44 | {
45 | var ast = Generate("List l = []; l.Insert(0, 69);");
46 | Assert.NotEmpty(ast.Statements);
47 |
48 | var statement = ast.Statements.Skip(2).First();
49 | Assert.IsType(statement);
50 |
51 | var expression = ((ExpressionStatement)statement).Expression;
52 | Assert.IsType(expression);
53 |
54 | var call = (Call)expression;
55 | Assert.NotNull(call.ExpandedByMacro);
56 | Assert.Equal(MacroKind.ListMethod, call.ExpandedByMacro);
57 | Assert.Equal(3, call.ArgumentList.Arguments.Count);
58 | Assert.IsType(call.Callee);
59 |
60 | var selfArgument = call.ArgumentList.Arguments[0].Expression;
61 | var indexArgument = call.ArgumentList.Arguments[1].Expression;
62 | var elementArgument = call.ArgumentList.Arguments[2].Expression;
63 | Assert.IsType(selfArgument);
64 | Assert.IsType(indexArgument);
65 | Assert.IsType(elementArgument);
66 | Assert.Equal("l", selfArgument.ToString());
67 |
68 | var index = (Literal)indexArgument;
69 | Assert.Equal("1", index.ValueText);
70 |
71 | var element = (Literal)elementArgument;
72 | Assert.Equal("69", element.ValueText);
73 |
74 | var memberAccess = (MemberAccess)call.Callee;
75 | Assert.IsType(memberAccess.Expression);
76 | Assert.IsType(memberAccess.Name);
77 | Assert.Equal("table", memberAccess.Expression.ToString());
78 | Assert.Equal("insert", memberAccess.Name.ToString());
79 | }
80 |
81 | [Fact]
82 | public void Macros_Clear()
83 | {
84 | var ast = Generate("List l = []; l.Clear();");
85 | Assert.NotEmpty(ast.Statements);
86 |
87 | var statement = ast.Statements.Skip(2).First();
88 | Assert.IsType(statement);
89 |
90 | var expression = ((ExpressionStatement)statement).Expression;
91 | Assert.IsType(expression);
92 |
93 | var call = (Call)expression;
94 | Assert.NotNull(call.ExpandedByMacro);
95 | Assert.Equal(MacroKind.ListMethod, call.ExpandedByMacro);
96 | Assert.Single(call.ArgumentList.Arguments);
97 | Assert.IsType(call.Callee);
98 |
99 | var selfArgument = call.ArgumentList.Arguments.First().Expression;
100 | Assert.IsType(selfArgument);
101 | Assert.Equal("l", selfArgument.ToString());
102 |
103 | var memberAccess = (MemberAccess)call.Callee;
104 | Assert.IsType(memberAccess.Expression);
105 | Assert.IsType(memberAccess.Name);
106 | Assert.Equal("table", memberAccess.Expression.ToString());
107 | Assert.Equal("clear", memberAccess.Name.ToString());
108 | }
109 |
110 | [Fact]
111 | public void Macros_Contains()
112 | {
113 | var ast = Generate("List l = []; l.Contains(69);");
114 | Assert.NotEmpty(ast.Statements);
115 |
116 | var statement = ast.Statements.Skip(2).First();
117 | Assert.IsType(statement);
118 |
119 | var expression = ((Variable)statement).Initializer;
120 | Assert.IsType(expression);
121 |
122 | var binaryOperator = (BinaryOperator)expression;
123 | Assert.NotNull(binaryOperator.ExpandedByMacro);
124 | Assert.Equal(MacroKind.ListMethod, binaryOperator.ExpandedByMacro);
125 | Assert.Equal("~=", binaryOperator.Operator);
126 | Assert.IsType(binaryOperator.Left);
127 | Assert.IsType(binaryOperator.Right);
128 |
129 | var call = (Call)binaryOperator.Left;
130 | Assert.Equal(2, call.ArgumentList.Arguments.Count);
131 | Assert.IsType(call.Callee);
132 |
133 | var selfArgument = call.ArgumentList.Arguments.First().Expression;
134 | var elementArgument = call.ArgumentList.Arguments.Last().Expression;
135 | Assert.IsType(selfArgument);
136 | Assert.IsType(elementArgument);
137 | Assert.Equal("l", selfArgument.ToString());
138 |
139 | var memberAccess = (MemberAccess)call.Callee;
140 | Assert.IsType(memberAccess.Expression);
141 | Assert.IsType(memberAccess.Name);
142 | Assert.Equal("table", memberAccess.Expression.ToString());
143 | Assert.Equal("find", memberAccess.Name.ToString());
144 | }
145 |
146 | [Fact]
147 | public void Macros_Remove()
148 | {
149 | var ast = Generate("List l = []; l.Remove(69);");
150 | Assert.NotEmpty(ast.Statements);
151 |
152 | var statement = ast.Statements.Skip(2).First();
153 | Assert.IsType(statement);
154 |
155 | var expression = ((Variable)statement).Initializer;
156 | Assert.IsType(expression);
157 |
158 | var call = (Call)expression;
159 | Assert.NotNull(call.ExpandedByMacro);
160 | Assert.Equal(MacroKind.ListMethod, call.ExpandedByMacro);
161 | Assert.Equal(2, call.ArgumentList.Arguments.Count);
162 | Assert.IsType(call.Callee);
163 |
164 | var selfArgument = call.ArgumentList.Arguments.First().Expression;
165 | var indexArgument = call.ArgumentList.Arguments.Last().Expression;
166 | Assert.Equal("l", selfArgument.ToString());
167 | Assert.IsType(selfArgument);
168 | Assert.IsType(indexArgument);
169 |
170 | var indexCall = (Call)indexArgument;
171 | Assert.Equal(2, indexCall.ArgumentList.Arguments.Count);
172 | Assert.IsType(indexCall.Callee);
173 |
174 | var elementArgument = indexCall.ArgumentList.Arguments.Last().Expression;
175 | var element = (Literal)elementArgument;
176 | Assert.Equal("69", element.ValueText);
177 |
178 | var indexMemberAccess = (MemberAccess)indexCall.Callee;
179 | Assert.IsType(indexMemberAccess.Expression);
180 | Assert.IsType(indexMemberAccess.Name);
181 | Assert.Equal("table", indexMemberAccess.Expression.ToString());
182 | Assert.Equal("find", indexMemberAccess.Name.ToString());
183 |
184 | var memberAccess = (MemberAccess)call.Callee;
185 | Assert.IsType(memberAccess.Expression);
186 | Assert.IsType(memberAccess.Name);
187 | Assert.Equal("table", memberAccess.Expression.ToString());
188 | Assert.Equal("remove", memberAccess.Name.ToString());
189 | }
190 |
191 | [Fact]
192 | public void Macros_RemoveAt()
193 | {
194 | var ast = Generate("List l = []; l.RemoveAt(0);");
195 | Assert.NotEmpty(ast.Statements);
196 |
197 | var statement = ast.Statements.Skip(2).First();
198 | Assert.IsType(statement);
199 |
200 | var expression = ((ExpressionStatement)statement).Expression;
201 | Assert.IsType(expression);
202 |
203 | var call = (Call)expression;
204 | Assert.NotNull(call.ExpandedByMacro);
205 | Assert.Equal(MacroKind.ListMethod, call.ExpandedByMacro);
206 | Assert.Equal(2, call.ArgumentList.Arguments.Count);
207 | Assert.IsType(call.Callee);
208 |
209 | var selfArgument = call.ArgumentList.Arguments.First().Expression;
210 | var indexArgument = call.ArgumentList.Arguments.Last().Expression;
211 | Assert.Equal("l", selfArgument.ToString());
212 | Assert.IsType(selfArgument);
213 | Assert.IsType(indexArgument);
214 |
215 | var index = (Literal)indexArgument;
216 | Assert.Equal("1", index.ValueText);
217 |
218 | var memberAccess = (MemberAccess)call.Callee;
219 | Assert.IsType(memberAccess.Expression);
220 | Assert.IsType(memberAccess.Name);
221 | Assert.Equal("table", memberAccess.Expression.ToString());
222 | Assert.Equal("remove", memberAccess.Name.ToString());
223 | }
224 |
225 | [Fact]
226 | public void Macros_AsReadOnly()
227 | {
228 | var ast = Generate("List l = []; l.AsReadOnly();");
229 | Assert.NotEmpty(ast.Statements);
230 |
231 | var statement = ast.Statements.Skip(2).First();
232 | Assert.IsType(statement);
233 |
234 | var expression = ((Variable)statement).Initializer;
235 | Assert.IsType(expression);
236 |
237 | var call = (Call)expression;
238 | Assert.NotNull(call.ExpandedByMacro);
239 | Assert.Equal(MacroKind.ListMethod, call.ExpandedByMacro);
240 | Assert.Single(call.ArgumentList.Arguments);
241 | Assert.IsType(call.Callee);
242 |
243 | var selfArgument = call.ArgumentList.Arguments.First().Expression;
244 | Assert.IsType(selfArgument);
245 | Assert.Equal("l", selfArgument.ToString());
246 |
247 | var memberAccess = (MemberAccess)call.Callee;
248 | Assert.IsType(memberAccess.Expression);
249 | Assert.IsType(memberAccess.Name);
250 | Assert.Equal("table", memberAccess.Expression.ToString());
251 | Assert.Equal("freeze", memberAccess.Name.ToString());
252 | }
253 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/MacroTests/ObjectMacrosTest.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Luau;
2 | using RobloxCS.Macros;
3 |
4 | namespace RobloxCS.Tests.MacroTests;
5 |
6 | public class ObjectMacrosTest : Base.Generation
7 | {
8 | [Fact]
9 | public void Macros_ToString()
10 | {
11 | var ast = Generate("(123).ToString();");
12 | Assert.NotEmpty(ast.Statements);
13 |
14 | var statement = ast.Statements.Skip(1).First();
15 | Assert.IsType(statement);
16 |
17 | var expression = ((Variable)statement).Initializer;
18 | Assert.NotNull(expression);
19 |
20 | var call = (Call)expression;
21 | Assert.NotNull(call.ExpandedByMacro);
22 | Assert.Equal(MacroKind.ObjectMethod, call.ExpandedByMacro);
23 | Assert.IsType(call.Callee);
24 |
25 | var identifierName = (IdentifierName)call.Callee;
26 | Assert.Equal("tostring", identifierName.ToString());
27 |
28 | var argument = call.ArgumentList.Arguments.First().Expression;
29 | Assert.IsType(argument);
30 |
31 | var parenthesized = (Parenthesized)argument;
32 | Assert.IsType(parenthesized.Expression);
33 |
34 | var literal = (Literal)parenthesized.Expression;
35 | Assert.Equal("123", literal.ValueText);
36 | }
37 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/RenderingTest.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Luau;
2 |
3 | namespace RobloxCS.Tests;
4 |
5 | public class RenderingTest
6 | {
7 | [Fact]
8 | public void Renders_AST()
9 | {
10 | var statement = new ExpressionStatement(AstUtility.PrintCall(AstUtility.String("bruh")));
11 | var ast = new AST([statement]);
12 | var output = Render(ast);
13 | const string expectedOutput = """
14 | print("bruh")
15 | return nil
16 |
17 | """;
18 |
19 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
20 | }
21 |
22 | [Fact]
23 | public void Renders_Block()
24 | {
25 | var statements = Enumerable.Repeat(new ExpressionStatement(AstUtility.PrintCall(AstUtility.String("bruh"))), 5)
26 | .ToList();
27 |
28 | var block = new Block(statements);
29 | var output = Render(block);
30 | const string expectedOutput = """
31 | print("bruh")
32 | print("bruh")
33 | print("bruh")
34 | print("bruh")
35 | print("bruh")
36 |
37 | """;
38 |
39 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
40 | }
41 |
42 | [Fact]
43 | public void Renders_ScopedBlock()
44 | {
45 | var statements = Enumerable.Repeat(new ExpressionStatement(AstUtility.PrintCall(AstUtility.String("bruh"))), 5)
46 | .ToList();
47 |
48 | var block = new ScopedBlock(statements);
49 | var output = Render(block);
50 | const string expectedOutput = """
51 | do
52 | print("bruh")
53 | print("bruh")
54 | print("bruh")
55 | print("bruh")
56 | print("bruh")
57 | end
58 |
59 | """;
60 |
61 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
62 | }
63 |
64 | [Fact]
65 | public void Renders_MultiLineLineComment()
66 | {
67 | var comment = new MultiLineComment(string.Join('\n', Enumerable.Repeat("roblox-cs is the best!", 5)));
68 | var output = Render(comment);
69 | const string expectedOutput = """
70 | --[[
71 | roblox-cs is the best!
72 | roblox-cs is the best!
73 | roblox-cs is the best!
74 | roblox-cs is the best!
75 | roblox-cs is the best!
76 | ]]
77 |
78 | """;
79 |
80 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
81 | }
82 |
83 | [Fact]
84 | public void Renders_SingleLineComment()
85 | {
86 | var comment = new SingleLineComment("roblox-cs is the best!");
87 | var output = Render(comment);
88 | const string expectedOutput = "-- roblox-cs is the best!";
89 |
90 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
91 | }
92 |
93 | [Fact]
94 | public void Renders_IterativeFor()
95 | {
96 | var valueName = new IdentifierName("value");
97 | var iterable = new IdentifierName("abc");
98 | var body = new ExpressionStatement(AstUtility.PrintCall(valueName));
99 | var forStatement = new For([AstUtility.DiscardName, valueName], iterable, body);
100 | var output = Render(forStatement);
101 | const string expectedOutput = """
102 | for _, value in abc do
103 | print(value)
104 | end
105 |
106 | """;
107 |
108 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
109 | }
110 |
111 | [Fact]
112 | public void Renders_NumericFor()
113 | {
114 | var name = new IdentifierName("i");
115 | var minimum = new Literal("420");
116 | var maximum = new Literal("69");
117 | var increment = new Literal("-1");
118 | var body = new ExpressionStatement(AstUtility.PrintCall(new Literal("\"balls\"")));
119 | var forStatement = new NumericFor(name,
120 | minimum,
121 | maximum,
122 | increment,
123 | body);
124 |
125 | var output = Render(forStatement);
126 | const string expectedOutput = """
127 | for i = 420, 69, -1 do
128 | print("balls")
129 | end
130 |
131 | """;
132 |
133 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
134 | }
135 |
136 | [Fact]
137 | public void Renders_Repeat()
138 | {
139 | var condition = new IdentifierName("balls");
140 | var body = new ExpressionStatement(AstUtility.PrintCall(new Literal("\"rah\"")));
141 | var repeatStatement = new Repeat(condition, body);
142 | var output = Render(repeatStatement);
143 | const string expectedOutput = """
144 | repeat
145 | print("rah")
146 | until balls
147 |
148 | """;
149 |
150 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
151 | }
152 |
153 | [Fact]
154 | public void Renders_While()
155 | {
156 | var condition = new IdentifierName("balls");
157 | var body = new ExpressionStatement(AstUtility.PrintCall(new Literal("\"rah\"")));
158 | var whileStatement = new While(condition, body);
159 | var output = Render(whileStatement);
160 | const string expectedOutput = """
161 | while balls do
162 | print("rah")
163 | end
164 |
165 | """;
166 |
167 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
168 | }
169 |
170 | [Fact]
171 | public void Renders_IfExpression()
172 | {
173 | var condition = new IdentifierName("runicIsCool");
174 | var body = new Literal("\"im tha best\"");
175 | var elseBranch = new Literal("\"im washed\"");
176 | var ifExpression = new IfExpression(condition, body, elseBranch, true);
177 | var output = Render(ifExpression);
178 | Assert.Equal("if runicIsCool then \"im tha best\" else \"im washed\"", output);
179 | }
180 |
181 | [Fact]
182 | public void Renders_If()
183 | {
184 | var identifier = new IdentifierName("balls");
185 | var condition1 = new BinaryOperator(identifier, "==", new Literal("69"));
186 | var condition2 = new BinaryOperator(identifier, "==", new Literal("420"));
187 | var body = new Block([new ExpressionStatement(AstUtility.PrintCall(new Literal("\"im tha best\"")))]);
188 | var elseBody = new Block([new ExpressionStatement(AstUtility.PrintCall(new Literal("\"uhhhh\"")))]);
189 | var elseifBody = new Block([new ExpressionStatement(AstUtility.PrintCall(new Literal("\"im washed\"")))]);
190 | var elseIfBranch = new Block([new If(condition2, elseifBody, elseBody)]);
191 | var ifStatement = new If(condition1, body, elseIfBranch);
192 | var output = Render(ifStatement);
193 | const string expectedOutput = """
194 | if balls == 69 then
195 | print("im tha best")
196 | elseif balls == 420 then
197 | print("im washed")
198 | else
199 | print("uhhhh")
200 | end
201 | """;
202 |
203 | Assert.Equal(expectedOutput.Replace("\r", "").Trim(), output.Replace("\r", "").Trim());
204 | }
205 |
206 | [Fact]
207 | public void Renders_EmptyTableInitializer()
208 | {
209 | var tableInitializer = new TableInitializer();
210 | var output = Render(tableInitializer);
211 |
212 | Assert.Equal("{}", output);
213 | }
214 |
215 | [Fact]
216 | public void Renders_ArrayTableInitializer()
217 | {
218 | var tableInitializer = new TableInitializer([new Literal("69"), new Literal("420"), AstUtility.String("abc")]);
219 | var output = Render(tableInitializer);
220 |
221 | Assert.Equal("{69, 420, \"abc\"}", output);
222 | }
223 |
224 | [Fact]
225 | public void Renders_DictionaryTableInitializer()
226 | {
227 | var tableInitializer = new TableInitializer([new Literal("69"), new Literal("420"), AstUtility.String("abc")],
228 | [new IdentifierName("foo"), new IdentifierName("bar"), AstUtility.String("baz")]);
229 |
230 | var output = Render(tableInitializer);
231 | const string expectedOutput = """
232 | {
233 | foo = 69,
234 | bar = 420,
235 | ["baz"] = "abc"
236 | }
237 | """;
238 |
239 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
240 | }
241 |
242 | [Fact]
243 | public void Renders_Calls()
244 | {
245 | var arguments = AstUtility.CreateArgumentList([new Literal("69"), new Literal("420"), AstUtility.String("abc")]);
246 | var call = new Call(new IdentifierName("bigMen"), arguments);
247 | var output = Render(call);
248 |
249 | Assert.Equal("bigMen(69, 420, \"abc\")", output);
250 | }
251 |
252 | [Fact]
253 | public void Renders_TypeOfCall()
254 | {
255 | var typeOfCall = new TypeOfCall(new IdentifierName("bigMen"));
256 | var output = Render(typeOfCall);
257 |
258 | Assert.Equal("typeof(bigMen)", output);
259 | }
260 |
261 | [Fact]
262 | public void Renders_IndexCall()
263 | {
264 | var indexCall = new IndexCall(new TypeRef("MyRecord"), new TypeRef("string"));
265 | var output = Render(indexCall);
266 |
267 | Assert.Equal("index", output);
268 | }
269 |
270 | [Fact]
271 | public void Renders_KeyOfCall()
272 | {
273 | var keyOfCall = new KeyOfCall(new TypeRef("MyRecord"));
274 | var output = Render(keyOfCall);
275 |
276 | Assert.Equal("keyof", output);
277 | }
278 |
279 | [Fact]
280 | public void Renders_ArgumentLists()
281 | {
282 | var arguments = AstUtility.CreateArgumentList([new Literal("69"), new Literal("420"), AstUtility.String("abc")]);
283 | var output = Render(arguments);
284 |
285 | Assert.Equal("(69, 420, \"abc\")", output);
286 | }
287 |
288 | [Fact]
289 | public void Renders_BuiltInAttributes()
290 | {
291 | var attribute = new AttributeList([new BuiltInAttribute(new IdentifierName("native"))]);
292 | var output = Render(attribute);
293 |
294 | Assert.Equal("@native\n", output.Replace("\r", ""));
295 | }
296 |
297 | [Fact]
298 | public void Renders_MappedTypes()
299 | {
300 | var mappedType = new MappedType(new TypeRef("string"), new TypeRef("number"));
301 | var output = Render(mappedType);
302 |
303 | Assert.Equal("{ [string]: number }", output);
304 | }
305 |
306 | [Fact]
307 | public void Renders_InterfaceTypes()
308 | {
309 | var interfaceType = new InterfaceType([new FieldType("myField", new TypeRef("string"), true)],
310 | null,
311 | false);
312 |
313 | var output = Render(interfaceType);
314 | const string expectedOutput = """
315 | {
316 | read myField: string;
317 | }
318 | """;
319 |
320 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
321 | }
322 |
323 | [Fact]
324 | public void Renders_OptionalTypes()
325 | {
326 | var optionalType = new OptionalType(new TypeRef("boolean"));
327 | var output = Render(optionalType);
328 |
329 | Assert.Equal("boolean?", output);
330 | }
331 |
332 | [Fact]
333 | public void Renders_FunctionTypes()
334 | {
335 | var functionType = new FunctionType([new ParameterType("myParam", new TypeRef("number"))],
336 | new TypeRef("boolean"));
337 |
338 | var output = Render(functionType);
339 | Assert.Equal("(myParam: number) -> boolean", output);
340 | }
341 |
342 | [Fact]
343 | public void Renders_ArrayTypes()
344 | {
345 | var arrayType = new ArrayType(new TypeRef("string"));
346 | var output = Render(arrayType);
347 |
348 | Assert.Equal("{ string }", output);
349 | }
350 |
351 | [Fact]
352 | public void Renders_TypeAliases()
353 | {
354 | var name = new IdentifierName("MyType");
355 | var value = new TypeRef("string");
356 | var typeAlias = new TypeAlias(name, value);
357 | var output = Render(typeAlias);
358 |
359 | Assert.Equal("type MyType = string\n", output);
360 | }
361 |
362 | [Fact]
363 | public void Renders_TypeCasts()
364 | {
365 | var value = new IdentifierName("myValue");
366 | var typeRef = new TypeRef("MyType");
367 | var typeCast = new TypeCast(value, typeRef);
368 | var output = Render(typeCast);
369 |
370 | Assert.Equal("myValue :: MyType", output);
371 | }
372 |
373 | [Fact]
374 | public void Renders_ElementAccess()
375 | {
376 | var elementAccess = new ElementAccess(new IdentifierName("a"), new Literal("123"));
377 | var output = Render(elementAccess);
378 |
379 | Assert.Equal("a[123]", output);
380 | }
381 |
382 | [Fact]
383 | public void Renders_MemberAccess()
384 | {
385 | var memberAccess = new MemberAccess(new IdentifierName("a"), new IdentifierName("b"));
386 | var output = Render(memberAccess);
387 |
388 | Assert.Equal("a.b", output);
389 | }
390 |
391 | [Fact]
392 | public void Renders_UnaryOperators()
393 | {
394 | var operand = new IdentifierName("isActive");
395 | var unaryOp = new UnaryOperator("not ", operand);
396 | var output = Render(unaryOp);
397 |
398 | Assert.Equal("not isActive", output);
399 | }
400 |
401 | [Fact]
402 | public void Renders_BinaryOperators()
403 | {
404 | var left = new Literal("69");
405 | var right = new Literal("420");
406 | var binaryOp = new BinaryOperator(left, "+", right);
407 | var output = Render(binaryOp);
408 |
409 | Assert.Equal("69 + 420", output);
410 | }
411 |
412 | [Fact]
413 | public void Renders_Assignment()
414 | {
415 | var target = new ElementAccess(new IdentifierName("a"), new Literal("69"));
416 | var value = new Literal("420");
417 | var assignment = new Assignment(target, value);
418 | var output = Render(assignment);
419 | Assert.Equal("a[69] = 420\n", output);
420 | }
421 |
422 | [Fact]
423 | public void Renders_QualifiedName()
424 | {
425 | const string result = "Abc:myMethod";
426 | var name = new QualifiedName(new IdentifierName("Abc"), new IdentifierName("myMethod"), ':');
427 | var output = Render(name);
428 |
429 | Assert.Equal(result, output);
430 | Assert.Equal(result, name.ToString());
431 | }
432 |
433 | [Fact]
434 | public void Renders_GenericName()
435 | {
436 | const string result = "Abc";
437 | var name = new GenericName("Abc", ["T", "U"]);
438 | var output = Render(name);
439 |
440 | Assert.Equal(result, output);
441 | Assert.Equal(result, name.ToString());
442 | }
443 |
444 | [Fact]
445 | public void Renders_IdentifierName()
446 | {
447 | const string text = "Abc";
448 | var name = new IdentifierName(text);
449 | var output = Render(name);
450 |
451 | Assert.Equal(text, output);
452 | Assert.Equal(text, name.ToString());
453 | }
454 |
455 | [Fact]
456 | public void Renders_Parenthesized()
457 | {
458 | var parenthesized = new Parenthesized(AstUtility.String("bruh"));
459 | var output = Render(parenthesized);
460 |
461 | Assert.Equal("(\"bruh\")", output);
462 | }
463 |
464 | [Fact]
465 | public void Renders_InterpolatedStrings()
466 | {
467 | var stringInterpolation = new InterpolatedString([new Literal("hello, "), new Interpolation(new IdentifierName("name")), new Literal("!"),]);
468 |
469 | var output = Render(stringInterpolation);
470 | Assert.Equal("`hello, {name}!`", output);
471 | }
472 |
473 | [Theory]
474 | [InlineData(true)]
475 | [InlineData(false)]
476 | public void Renders_Variables(bool isLocal)
477 | {
478 | var identifier = new IdentifierName("abc");
479 | var value = new Literal("69");
480 | var typeRef = new TypeRef("number");
481 | var variable = new Variable(identifier, isLocal, value, typeRef);
482 | var output = Render(variable);
483 | Assert.Equal($"{(isLocal ? "local " : "")}abc: number = 69\n", output);
484 | }
485 |
486 | [Fact]
487 | public void Renders_VariableLists()
488 | {
489 | var identifier = new IdentifierName("abc");
490 | var value = new Literal("69");
491 | var typeRef = new TypeRef("number");
492 | var variables = Enumerable.Repeat(new Variable(identifier, true, value, typeRef), 5).ToList();
493 | var variableList = new VariableList(variables);
494 | var output = Render(variableList);
495 | const string expectedOutput = """
496 | local abc: number = 69
497 | local abc: number = 69
498 | local abc: number = 69
499 | local abc: number = 69
500 | local abc: number = 69
501 |
502 | """;
503 |
504 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
505 | }
506 |
507 | [Fact]
508 | public void Renders_ParametersWithDefault()
509 | {
510 | var identifier = new IdentifierName("myFunction");
511 | var parameterIdentifier = new IdentifierName("x");
512 | var parameterType = new OptionalType(new TypeRef("number"));
513 | var parameterDefault = new Literal("69");
514 | var body = new Block([]);
515 | var parameter = new Parameter(parameterIdentifier, false, parameterDefault, parameterType);
516 | var parameters = new ParameterList([parameter]);
517 | var function = new Function(identifier,
518 | true,
519 | parameters,
520 | null,
521 | body);
522 |
523 | var output = Render(function);
524 | const string expectedOutput = """
525 | local function myFunction(x: number?)
526 | if x == nil then
527 | x = 69
528 | end
529 | end
530 |
531 | """;
532 |
533 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
534 | }
535 |
536 | [Fact]
537 | public void Renders_StandardParameters()
538 | {
539 | var identifier = new IdentifierName("myFunction");
540 | var parameterIdentifier = new IdentifierName("x");
541 | var parameterType = new TypeRef("number");
542 | var body = new Block([]);
543 | var parameter = new Parameter(parameterIdentifier, false, null, parameterType);
544 | var parameters = new ParameterList([parameter]);
545 | var function = new Function(identifier,
546 | true,
547 | parameters,
548 | null,
549 | body);
550 |
551 | var output = Render(function);
552 | const string expectedOutput = """
553 | local function myFunction(x: number)
554 | end
555 |
556 | """;
557 |
558 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
559 | }
560 |
561 | [Fact]
562 | public void Renders_VarargParameters()
563 | {
564 | var identifier = new IdentifierName("myFunction");
565 | var parameterIdentifier = new IdentifierName("args");
566 | var parameterType = new TypeRef("number");
567 | var body = new Block([]);
568 | var parameter = new Parameter(parameterIdentifier, true, null, parameterType);
569 | var parameters = new ParameterList([parameter]);
570 | var function = new Function(identifier,
571 | true,
572 | parameters,
573 | null,
574 | body);
575 |
576 | var output = Render(function);
577 | const string expectedOutput = """
578 | local function myFunction(...: number)
579 | local args: { number } = {...}
580 | end
581 |
582 | """;
583 |
584 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
585 | }
586 |
587 | [Fact]
588 | public void Renders_AnonymousFunctions()
589 | {
590 | var body = new Block([new Return(new Literal("69"))]);
591 | var function = new AnonymousFunction(new ParameterList([]), new TypeRef("number"), body);
592 | var output = Render(function);
593 | const string expectedOutput = """
594 | function(): number
595 | return 69
596 | end
597 | """;
598 |
599 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
600 | }
601 |
602 | [Theory]
603 | [InlineData(true)]
604 | [InlineData(false)]
605 | public void Renders_Functions(bool isLocal)
606 | {
607 | var identifier = new IdentifierName("myFunction");
608 | var body = new Block([new Return(new Literal("69"))]);
609 | var returnType = new TypeRef("number");
610 | var function = new Function(identifier,
611 | isLocal,
612 | new ParameterList([]),
613 | returnType,
614 | body);
615 |
616 | var output = Render(function);
617 | var expectedOutput = $"""
618 | {(isLocal ? "local " : "")}function myFunction(): number
619 | return 69
620 | end
621 |
622 | """;
623 |
624 | Assert.Equal(expectedOutput.Replace("\r", ""), output.Replace("\r", ""));
625 | }
626 |
627 | [Fact]
628 | public void Renders_Continue()
629 | {
630 | var @continue = new Continue();
631 | var output = Render(@continue);
632 |
633 | Assert.Equal("continue\n", output.Replace("\r", ""));
634 | }
635 |
636 | [Fact]
637 | public void Renders_Break()
638 | {
639 | var @break = new Break();
640 | var output = Render(@break);
641 |
642 | Assert.Equal("break\n", output.Replace("\r", ""));
643 | }
644 |
645 | private static string Render(Node node)
646 | {
647 | var writer = new LuauWriter();
648 | node.Render(writer);
649 |
650 | return writer.ToString();
651 | }
652 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/RobloxCS.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/RobloxCS.Tests/StandardUtilityTest.cs:
--------------------------------------------------------------------------------
1 | using RobloxCS.Shared;
2 |
3 | namespace RobloxCS.Tests;
4 |
5 | public class StandardUtilityTest
6 | {
7 | [Theory]
8 | [InlineData("float", "0")]
9 | [InlineData("double", "0")]
10 | [InlineData("int", "0")]
11 | [InlineData("uint", "0")]
12 | [InlineData("short", "0")]
13 | [InlineData("ushort", "0")]
14 | [InlineData("byte", "0")]
15 | [InlineData("sbyte", "0")]
16 | [InlineData("string", "\"\"")]
17 | [InlineData("char", "\"\"")]
18 | [InlineData("bool", "false")]
19 | [InlineData("nil", "nil")]
20 | [InlineData("WhatTheFuck", "nil")]
21 | public void GetDefaultValueForType(string typeName, string expectedValueText)
22 | {
23 | var valueText = StandardUtility.GetDefaultValueForType(typeName);
24 | Assert.Equal(expectedValueText, valueText);
25 | }
26 |
27 | [Theory]
28 | [InlineData("abc", "Abc")]
29 | [InlineData("Abc", "Abc")]
30 | [InlineData("ABC", "ABC")]
31 | [InlineData("", "")]
32 | public void Capitalize(string input, string expectedOutput)
33 | {
34 | var output = StandardUtility.Capitalize(input);
35 | Assert.Equal(expectedOutput, output);
36 | }
37 |
38 | [Theory]
39 | [InlineData("&", "band")]
40 | [InlineData("&=", "band")]
41 | [InlineData("|", "bor")]
42 | [InlineData("|=", "bor")]
43 | [InlineData("^", "bxor")]
44 | [InlineData("^=", "bxor")]
45 | [InlineData(">>", "rshift")]
46 | [InlineData(">>=", "rshift")]
47 | [InlineData("<<", "lshift")]
48 | [InlineData("<<=", "lshift")]
49 | [InlineData(">>>", "arshift")]
50 | [InlineData(">>>=", "arshift")]
51 | [InlineData("~", "bnot")]
52 | public void GetBit32MethodName(string input, string expectedOutput)
53 | {
54 | var output = StandardUtility.GetBit32MethodName(input);
55 | Assert.Equal(expectedOutput, output);
56 | }
57 |
58 | [Theory]
59 | [InlineData("A", "B", "C")]
60 | [InlineData("A", "B")]
61 | [InlineData("A>", "B")]
62 | [InlineData("A>", "D", "B")]
63 | [InlineData("A, D>", "B", "D")]
64 | [InlineData("A, D>", "B", "D")]
65 | public void ExtractTypeArguments(string input, params string[] expectedOutput)
66 | {
67 | var output = StandardUtility.ExtractTypeArguments(input);
68 | Assert.Equal(expectedOutput.Length, output.Count);
69 |
70 | for (var i = 0; i < expectedOutput.Length; i++)
71 | Assert.Equal(expectedOutput[i], output[i]);
72 | }
73 |
74 | [Theory]
75 | [InlineData("++", "+=")]
76 | [InlineData("--", "-=")]
77 | [InlineData("!=", "~=")]
78 | [InlineData("!", "not ")]
79 | [InlineData("&&", "and")]
80 | [InlineData("||", "or")]
81 | [InlineData("*", "*")]
82 | public void GetMappedOperator(string input, string expectedOutput)
83 | {
84 | var output = StandardUtility.GetMappedOperator(input);
85 | Assert.Equal(expectedOutput, output);
86 | }
87 | }
--------------------------------------------------------------------------------
/RobloxCS.Tests/TransformerTests/MainTransformerTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 | using Microsoft.CodeAnalysis.CSharp.Syntax;
3 | using RobloxCS.Luau;
4 | using RobloxCS.Shared;
5 | using RobloxCS.Transformers;
6 |
7 | namespace RobloxCS.Tests.TransformerTests;
8 |
9 | public class MainTransformerTest
10 | {
11 | [Fact]
12 | public void Transforms_PrimaryConstructors()
13 | {
14 | const string source = """
15 | class MyClass(int abc, string def)
16 | {
17 | public int Abc { get; } = abc;
18 | public string Def { get; } = def;
19 | }
20 | """;
21 |
22 | var compilationUnit = Transform(source);
23 | var classDecl = compilationUnit.Members.OfType().Single();
24 | Assert.Null(classDecl.ParameterList); // removed primary ctor
25 |
26 | var properties = classDecl.Members.OfType().ToArray();
27 | Assert.Equal(2, properties.Length);
28 | Assert.Contains(properties, p => p.Identifier.Text == "Abc");
29 | Assert.Contains(properties, p => p.Identifier.Text == "Def");
30 | Assert.All(properties, p => Assert.Null(p.Initializer)); // props using primary params have no initializer
31 |
32 | var constructors = classDecl.Members.OfType