├── .github └── workflows │ └── build.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CSharpToSwift.csproj ├── Program.cs ├── README.md ├── TranspileExpression.cs ├── TranspileStatement.cs ├── TranspileType.cs ├── Transpiler.cs └── global.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 6.0.300 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | #- name: Test 28 | # run: dotnet test --no-build --verbosity normal 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from `dotnet new gitignore` 5 | 6 | # dotenv files 7 | .env 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # Tye 69 | .tye/ 70 | 71 | # ASP.NET Scaffolding 72 | ScaffoldingReadMe.txt 73 | 74 | # StyleCop 75 | StyleCopReport.xml 76 | 77 | # Files built by Visual Studio 78 | *_i.c 79 | *_p.c 80 | *_h.h 81 | *.ilk 82 | *.meta 83 | *.obj 84 | *.iobj 85 | *.pch 86 | *.pdb 87 | *.ipdb 88 | *.pgc 89 | *.pgd 90 | *.rsp 91 | *.sbr 92 | *.tlb 93 | *.tli 94 | *.tlh 95 | *.tmp 96 | *.tmp_proj 97 | *_wpftmp.csproj 98 | *.log 99 | *.tlog 100 | *.vspscc 101 | *.vssscc 102 | .builds 103 | *.pidb 104 | *.svclog 105 | *.scc 106 | 107 | # Chutzpah Test files 108 | _Chutzpah* 109 | 110 | # Visual C++ cache files 111 | ipch/ 112 | *.aps 113 | *.ncb 114 | *.opendb 115 | *.opensdf 116 | *.sdf 117 | *.cachefile 118 | *.VC.db 119 | *.VC.VC.opendb 120 | 121 | # Visual Studio profiler 122 | *.psess 123 | *.vsp 124 | *.vspx 125 | *.sap 126 | 127 | # Visual Studio Trace Files 128 | *.e2e 129 | 130 | # TFS 2012 Local Workspace 131 | $tf/ 132 | 133 | # Guidance Automation Toolkit 134 | *.gpState 135 | 136 | # ReSharper is a .NET coding add-in 137 | _ReSharper*/ 138 | *.[Rr]e[Ss]harper 139 | *.DotSettings.user 140 | 141 | # TeamCity is a build add-in 142 | _TeamCity* 143 | 144 | # DotCover is a Code Coverage Tool 145 | *.dotCover 146 | 147 | # AxoCover is a Code Coverage Tool 148 | .axoCover/* 149 | !.axoCover/settings.json 150 | 151 | # Coverlet is a free, cross platform Code Coverage Tool 152 | coverage*.json 153 | coverage*.xml 154 | coverage*.info 155 | 156 | # Visual Studio code coverage results 157 | *.coverage 158 | *.coveragexml 159 | 160 | # NCrunch 161 | _NCrunch_* 162 | .*crunch*.local.xml 163 | nCrunchTemp_* 164 | 165 | # MightyMoose 166 | *.mm.* 167 | AutoTest.Net/ 168 | 169 | # Web workbench (sass) 170 | .sass-cache/ 171 | 172 | # Installshield output folder 173 | [Ee]xpress/ 174 | 175 | # DocProject is a documentation generator add-in 176 | DocProject/buildhelp/ 177 | DocProject/Help/*.HxT 178 | DocProject/Help/*.HxC 179 | DocProject/Help/*.hhc 180 | DocProject/Help/*.hhk 181 | DocProject/Help/*.hhp 182 | DocProject/Help/Html2 183 | DocProject/Help/html 184 | 185 | # Click-Once directory 186 | publish/ 187 | 188 | # Publish Web Output 189 | *.[Pp]ublish.xml 190 | *.azurePubxml 191 | # Note: Comment the next line if you want to checkin your web deploy settings, 192 | # but database connection strings (with potential passwords) will be unencrypted 193 | *.pubxml 194 | *.publishproj 195 | 196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 197 | # checkin your Azure Web App publish settings, but sensitive information contained 198 | # in these scripts will be unencrypted 199 | PublishScripts/ 200 | 201 | # NuGet Packages 202 | *.nupkg 203 | # NuGet Symbol Packages 204 | *.snupkg 205 | # The packages folder can be ignored because of Package Restore 206 | **/[Pp]ackages/* 207 | # except build/, which is used as an MSBuild target. 208 | !**/[Pp]ackages/build/ 209 | # Uncomment if necessary however generally it will be regenerated when needed 210 | #!**/[Pp]ackages/repositories.config 211 | # NuGet v3's project.json files produces more ignorable files 212 | *.nuget.props 213 | *.nuget.targets 214 | 215 | # Microsoft Azure Build Output 216 | csx/ 217 | *.build.csdef 218 | 219 | # Microsoft Azure Emulator 220 | ecf/ 221 | rcf/ 222 | 223 | # Windows Store app package directories and files 224 | AppPackages/ 225 | BundleArtifacts/ 226 | Package.StoreAssociation.xml 227 | _pkginfo.txt 228 | *.appx 229 | *.appxbundle 230 | *.appxupload 231 | 232 | # Visual Studio cache files 233 | # files ending in .cache can be ignored 234 | *.[Cc]ache 235 | # but keep track of directories ending in .cache 236 | !?*.[Cc]ache/ 237 | 238 | # Others 239 | ClientBin/ 240 | ~$* 241 | *~ 242 | *.dbmdl 243 | *.dbproj.schemaview 244 | *.jfm 245 | *.pfx 246 | *.publishsettings 247 | orleans.codegen.cs 248 | 249 | # Including strong name files can present a security risk 250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 251 | #*.snk 252 | 253 | # Since there are multiple workflows, uncomment next line to ignore bower_components 254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 255 | #bower_components/ 256 | 257 | # RIA/Silverlight projects 258 | Generated_Code/ 259 | 260 | # Backup & report files from converting an old project file 261 | # to a newer Visual Studio version. Backup files are not needed, 262 | # because we have git ;-) 263 | _UpgradeReport_Files/ 264 | Backup*/ 265 | UpgradeLog*.XML 266 | UpgradeLog*.htm 267 | ServiceFabricBackup/ 268 | *.rptproj.bak 269 | 270 | # SQL Server files 271 | *.mdf 272 | *.ldf 273 | *.ndf 274 | 275 | # Business Intelligence projects 276 | *.rdl.data 277 | *.bim.layout 278 | *.bim_*.settings 279 | *.rptproj.rsuser 280 | *- [Bb]ackup.rdl 281 | *- [Bb]ackup ([0-9]).rdl 282 | *- [Bb]ackup ([0-9][0-9]).rdl 283 | 284 | # Microsoft Fakes 285 | FakesAssemblies/ 286 | 287 | # GhostDoc plugin setting file 288 | *.GhostDoc.xml 289 | 290 | # Node.js Tools for Visual Studio 291 | .ntvs_analysis.dat 292 | node_modules/ 293 | 294 | # Visual Studio 6 build log 295 | *.plg 296 | 297 | # Visual Studio 6 workspace options file 298 | *.opt 299 | 300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 301 | *.vbw 302 | 303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 304 | *.vbp 305 | 306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 307 | *.dsw 308 | *.dsp 309 | 310 | # Visual Studio 6 technical files 311 | *.ncb 312 | *.aps 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # CodeRush personal settings 330 | .cr/personal 331 | 332 | # Python Tools for Visual Studio (PTVS) 333 | __pycache__/ 334 | *.pyc 335 | 336 | # Cake - Uncomment if you are using it 337 | # tools/** 338 | # !tools/packages.config 339 | 340 | # Tabs Studio 341 | *.tss 342 | 343 | # Telerik's JustMock configuration file 344 | *.jmconfig 345 | 346 | # BizTalk build output 347 | *.btp.cs 348 | *.btm.cs 349 | *.odx.cs 350 | *.xsd.cs 351 | 352 | # OpenCover UI analysis results 353 | OpenCover/ 354 | 355 | # Azure Stream Analytics local run output 356 | ASALocalRun/ 357 | 358 | # MSBuild Binary and Structured Log 359 | *.binlog 360 | 361 | # NVidia Nsight GPU debugger configuration file 362 | *.nvuser 363 | 364 | # MFractors (Xamarin productivity tool) working folder 365 | .mfractor/ 366 | 367 | # Local History for Visual Studio 368 | .localhistory/ 369 | 370 | # Visual Studio History (VSHistory) files 371 | .vshistory/ 372 | 373 | # BeatPulse healthcheck temp database 374 | healthchecksdb 375 | 376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 377 | MigrationBackup/ 378 | 379 | # Ionide (cross platform F# VS Code tools) working folder 380 | .ionide/ 381 | 382 | # Fody - auto-generated XML schema 383 | FodyWeavers.xsd 384 | 385 | # VS Code files for those working on multiple tools 386 | .vscode/* 387 | !.vscode/settings.json 388 | !.vscode/tasks.json 389 | !.vscode/launch.json 390 | !.vscode/extensions.json 391 | *.code-workspace 392 | 393 | # Local History for Visual Studio Code 394 | .history/ 395 | 396 | # Windows Installer files from build outputs 397 | *.cab 398 | *.msi 399 | *.msix 400 | *.msm 401 | *.msp 402 | 403 | # JetBrains Rider 404 | *.sln.iml 405 | .idea 406 | 407 | ## 408 | ## Visual studio for Mac 409 | ## 410 | 411 | 412 | # globs 413 | Makefile.in 414 | *.userprefs 415 | *.usertasks 416 | config.make 417 | config.status 418 | aclocal.m4 419 | install-sh 420 | autom4te.cache/ 421 | *.tar.gz 422 | tarballs/ 423 | test-results/ 424 | 425 | # Mac bundle stuff 426 | *.dmg 427 | *.app 428 | 429 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 430 | # General 431 | .DS_Store 432 | .AppleDouble 433 | .LSOverride 434 | 435 | # Icon must end with two \r 436 | Icon 437 | 438 | 439 | # Thumbnails 440 | ._* 441 | 442 | # Files that might appear in the root of a volume 443 | .DocumentRevisions-V100 444 | .fseventsd 445 | .Spotlight-V100 446 | .TemporaryItems 447 | .Trashes 448 | .VolumeIcon.icns 449 | .com.apple.timemachine.donotpresent 450 | 451 | # Directories potentially created on remote AFP share 452 | .AppleDB 453 | .AppleDesktop 454 | Network Trash Folder 455 | Temporary Items 456 | .apdisk 457 | 458 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 459 | # Windows thumbnail cache files 460 | Thumbs.db 461 | ehthumbs.db 462 | ehthumbs_vista.db 463 | 464 | # Dump file 465 | *.stackdump 466 | 467 | # Folder config file 468 | [Dd]esktop.ini 469 | 470 | # Recycle Bin used on file shares 471 | $RECYCLE.BIN/ 472 | 473 | # Windows Installer files 474 | *.cab 475 | *.msi 476 | *.msix 477 | *.msm 478 | *.msp 479 | 480 | # Windows shortcuts 481 | *.lnk 482 | 483 | # Vim temporary swap files 484 | *.swp 485 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/net6.0/CSharpToSwift.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/CSharpToSwift.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/CSharpToSwift.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/CSharpToSwift.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /CSharpToSwift.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | true 10 | cs2swift 11 | .\PackageOutput 12 | CSharpToSwift 13 | Convert C# code to Swift 14 | README.md 15 | Swift;iOS;Mac;Xamarin; 16 | https://github.com/praeclarum/CSharpToSwift 17 | MIT 18 | true 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using CSharpToSwift; 2 | 3 | Console.WriteLine("C# to Swift Transpiler"); 4 | 5 | static void PrintHelp() { 6 | Console.WriteLine("Usage: csharp2swift "); 7 | } 8 | 9 | if (args.Length < 2) { 10 | PrintHelp(); 11 | return 1; 12 | } 13 | string projectFilePath = args[0]; 14 | if (!File.Exists(projectFilePath)) { 15 | Console.WriteLine($"Project file {projectFilePath} does not exist."); 16 | return 1; 17 | } 18 | string outputDir = args[1]; 19 | if (!Directory.Exists(outputDir)) { 20 | Directory.CreateDirectory(outputDir); 21 | } 22 | 23 | try { 24 | var transpiler = new Transpiler(projectFilePath, outputDir); 25 | await transpiler.TranspileAsync(); 26 | return 0; 27 | } 28 | catch (Exception ex) { 29 | Console.WriteLine(ex.Message); 30 | return 2; 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C# to Swift Transpiler 2 | 3 | Performs a source to source translation of C# code to Swift. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | cs2swift path/to/your/project.csproj 9 | ``` 10 | -------------------------------------------------------------------------------- /TranspileExpression.cs: -------------------------------------------------------------------------------- 1 | namespace CSharpToSwift; 2 | 3 | using System; 4 | using System.Text.RegularExpressions; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | 9 | partial class Transpiler { 10 | string TranspileExpression(ExpressionSyntax value, SemanticModel model, string indent = "") 11 | { 12 | switch (value.Kind ()) { 13 | case SyntaxKind.AddExpression: 14 | var add = (BinaryExpressionSyntax)value; 15 | return $"{TranspileExpression(add.Left, model)} + {TranspileExpression(add.Right, model)}"; 16 | case SyntaxKind.AddAssignmentExpression: 17 | var addAssign = (AssignmentExpressionSyntax)value; 18 | return $"{TranspileExpression(addAssign.Left, model)} += {TranspileExpression(addAssign.Right, model)}"; 19 | case SyntaxKind.AndAssignmentExpression: 20 | var andAssign = (AssignmentExpressionSyntax)value; 21 | return $"{TranspileExpression(andAssign.Left, model)} &= {TranspileExpression(andAssign.Right, model)}"; 22 | case SyntaxKind.ArrayCreationExpression: 23 | return TranspileArrayCreation((ArrayCreationExpressionSyntax)value, model, indent); 24 | case SyntaxKind.ArrayInitializerExpression: 25 | var aiElements = string.Join(", ", ((InitializerExpressionSyntax)value).Expressions.Select(x => TranspileExpression(x, model, indent))); 26 | return $"[{aiElements}]"; 27 | case SyntaxKind.AsExpression: 28 | var ase = (BinaryExpressionSyntax)value; 29 | return $"{TranspileExpression(ase.Left, model)} as? {TranspileExpression(ase.Right, model)}"; 30 | case SyntaxKind.BaseExpression: 31 | return "super"; 32 | case SyntaxKind.BitwiseAndExpression: 33 | var bitAnd = (BinaryExpressionSyntax)value; 34 | return $"{TranspileExpression(bitAnd.Left, model)} & {TranspileExpression(bitAnd.Right, model)}"; 35 | case SyntaxKind.BitwiseOrExpression: 36 | var bitOr = (BinaryExpressionSyntax)value; 37 | return $"{TranspileExpression(bitOr.Left, model)} | {TranspileExpression(bitOr.Right, model)}"; 38 | case SyntaxKind.BitwiseNotExpression: 39 | var bitNot = (PrefixUnaryExpressionSyntax)value; 40 | return $"~{TranspileExpression(bitNot.Operand, model)}"; 41 | case SyntaxKind.CastExpression: 42 | var cast = (CastExpressionSyntax)value; 43 | return $"{TranspileExpression(cast.Expression, model)} as {GetSwiftTypeName (cast.Type, model)}"; 44 | case SyntaxKind.CharacterLiteralExpression: 45 | var charLit = (LiteralExpressionSyntax)value; 46 | return charLit.Token.Text.Replace('\'', '\"'); 47 | case SyntaxKind.CoalesceExpression: 48 | var coalesce = (BinaryExpressionSyntax)value; 49 | return $"{TranspileExpression(coalesce.Left, model)} ?? {TranspileExpression(coalesce.Right, model)}"; 50 | case SyntaxKind.ConditionalAccessExpression: 51 | var ca = (ConditionalAccessExpressionSyntax)value; 52 | return $"{TranspileExpression(ca.Expression, model)}?{TranspileExpression(ca.WhenNotNull, model)}"; 53 | case SyntaxKind.ConditionalExpression: 54 | var cond = (ConditionalExpressionSyntax)value; 55 | return $"{TranspileExpression(cond.Condition, model)} ? {TranspileExpression(cond.WhenTrue, model)} : {TranspileExpression(cond.WhenFalse, model)}"; 56 | case SyntaxKind.DivideExpression: 57 | var div = (BinaryExpressionSyntax)value; 58 | return $"{TranspileExpression(div.Left, model)} / {TranspileExpression(div.Right, model)}"; 59 | case SyntaxKind.DefaultExpression: 60 | var def = (DefaultExpressionSyntax)value; 61 | return GetDefaultValue(model.GetTypeInfo(def.Type).Type); 62 | case SyntaxKind.DivideAssignmentExpression: 63 | var divAssign = (AssignmentExpressionSyntax)value; 64 | return $"{TranspileExpression(divAssign.Left, model)} /= {TranspileExpression(divAssign.Right, model)}"; 65 | case SyntaxKind.ElementAccessExpression: 66 | var ea = (ElementAccessExpressionSyntax)value; 67 | var eaArgs = TranspileElementArguments(ea.ArgumentList, model, indent); 68 | return $"{TranspileExpression(ea.Expression, model)}[{eaArgs}]"; 69 | case SyntaxKind.ElementBindingExpression: 70 | var eb = (ElementBindingExpressionSyntax)value; 71 | var ebArgs = TranspileElementArguments(eb.ArgumentList, model, indent); 72 | return $"[{ebArgs}]"; 73 | case SyntaxKind.EqualsExpression: 74 | var eq = (BinaryExpressionSyntax)value; 75 | return $"{TranspileExpression(eq.Left, model)} == {TranspileExpression(eq.Right, model)}"; 76 | case SyntaxKind.ExclusiveOrExpression: 77 | var xor = (BinaryExpressionSyntax)value; 78 | return $"{TranspileExpression(xor.Left, model)} ^ {TranspileExpression(xor.Right, model)}"; 79 | case SyntaxKind.FalseLiteralExpression: 80 | return "false"; 81 | case SyntaxKind.GreaterThanExpression: 82 | var gt = (BinaryExpressionSyntax)value; 83 | return $"{TranspileExpression(gt.Left, model)} > {TranspileExpression(gt.Right, model)}"; 84 | case SyntaxKind.GreaterThanOrEqualExpression: 85 | var gte = (BinaryExpressionSyntax)value; 86 | return $"{TranspileExpression(gte.Left, model)} >= {TranspileExpression(gte.Right, model)}"; 87 | case SyntaxKind.IdentifierName: 88 | var id = (IdentifierNameSyntax)value; 89 | return id.Identifier.ToString(); 90 | case SyntaxKind.ImplicitArrayCreationExpression: 91 | var iac = (ImplicitArrayCreationExpressionSyntax)value; 92 | var iacArgs = string.Join(", ", iac.Initializer.Expressions.Select(e => TranspileExpression(e, model, indent))); 93 | return $"[{iacArgs}]"; 94 | case SyntaxKind.InvocationExpression: 95 | var inv = (InvocationExpressionSyntax)value; 96 | return TranspileInvocation(TranspileExpression(inv.Expression, model), inv, inv.ArgumentList, model, indent); 97 | case SyntaxKind.IsExpression: 98 | var ise = (BinaryExpressionSyntax)value; 99 | return $"{TranspileExpression(ise.Left, model)} is {TranspileExpression(ise.Right, model)}"; 100 | case SyntaxKind.IsPatternExpression: 101 | return TranspileIsPattern((IsPatternExpressionSyntax)value, model, indent); 102 | case SyntaxKind.LeftShiftExpression: 103 | var lshift = (BinaryExpressionSyntax)value; 104 | return $"{TranspileExpression(lshift.Left, model)} << {TranspileExpression(lshift.Right, model)}"; 105 | case SyntaxKind.LeftShiftAssignmentExpression: 106 | var lshiftAssign = (AssignmentExpressionSyntax)value; 107 | return $"{TranspileExpression(lshiftAssign.Left, model)} <<= {TranspileExpression(lshiftAssign.Right, model)}"; 108 | case SyntaxKind.LessThanExpression: 109 | var lt = (BinaryExpressionSyntax)value; 110 | return $"{TranspileExpression(lt.Left, model)} < {TranspileExpression(lt.Right, model)}"; 111 | case SyntaxKind.LessThanOrEqualExpression: 112 | var lte = (BinaryExpressionSyntax)value; 113 | return $"{TranspileExpression(lte.Left, model)} <= {TranspileExpression(lte.Right, model)}"; 114 | case SyntaxKind.LogicalAndExpression: 115 | var and = (BinaryExpressionSyntax)value; 116 | return $"{TranspileExpression(and.Left, model)} && {TranspileExpression(and.Right, model)}"; 117 | case SyntaxKind.LogicalNotExpression: 118 | var not = (PrefixUnaryExpressionSyntax)value; 119 | return $"!{TranspileExpression(not.Operand, model)}"; 120 | case SyntaxKind.LogicalOrExpression: 121 | var or = (BinaryExpressionSyntax)value; 122 | return $"{TranspileExpression(or.Left, model)} || {TranspileExpression(or.Right, model)}"; 123 | case SyntaxKind.MemberBindingExpression: 124 | var mbe = (MemberBindingExpressionSyntax)value; 125 | return $".{TranspileExpression(mbe.Name, model)}"; 126 | case SyntaxKind.ModuloExpression: 127 | var mod = (BinaryExpressionSyntax)value; 128 | return $"{TranspileExpression(mod.Left, model)} % {TranspileExpression(mod.Right, model)}"; 129 | case SyntaxKind.MultiplyExpression: 130 | var mul = (BinaryExpressionSyntax)value; 131 | return $"{TranspileExpression(mul.Left, model)} * {TranspileExpression(mul.Right, model)}"; 132 | case SyntaxKind.MultiplyAssignmentExpression: 133 | var mulAssign = (AssignmentExpressionSyntax)value; 134 | return $"{TranspileExpression(mulAssign.Left, model)} *= {TranspileExpression(mulAssign.Right, model)}"; 135 | case SyntaxKind.NotEqualsExpression: 136 | var neq = (BinaryExpressionSyntax)value; 137 | return $"{TranspileExpression(neq.Left, model)} != {TranspileExpression(neq.Right, model)}"; 138 | case SyntaxKind.NullLiteralExpression: 139 | return "nil"; 140 | case SyntaxKind.NumericLiteralExpression: 141 | var nlit = (LiteralExpressionSyntax)value; 142 | { 143 | var ntext = nlit.Token.Text; 144 | if (ntext[0] == '.') 145 | ntext = "0" + ntext; 146 | if (ntext[^1] == 'f' && !ntext.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)) 147 | ntext = ntext.Substring(0, ntext.Length - 1); 148 | if (ntext.EndsWith("ul", StringComparison.InvariantCultureIgnoreCase)) 149 | ntext = $"UInt64({ntext.Substring(0, ntext.Length - 2)})"; 150 | else if (ntext.EndsWith("l", StringComparison.InvariantCultureIgnoreCase)) 151 | ntext = $"Int64({ntext.Substring(0, ntext.Length - 1)})"; 152 | else if (ntext.EndsWith("u", StringComparison.InvariantCultureIgnoreCase)) 153 | ntext = $"UInt32({ntext.Substring(0, ntext.Length - 1)})"; 154 | else if (ntext.EndsWith("m", StringComparison.InvariantCultureIgnoreCase)) 155 | ntext = $"Int32({ntext.Substring(0, ntext.Length - 1)})"; 156 | return ntext; 157 | } 158 | case SyntaxKind.ObjectCreationExpression: 159 | return TranspileObjectCreationExpression((ObjectCreationExpressionSyntax)value, model, indent); 160 | case SyntaxKind.OrAssignmentExpression: 161 | var orAssign = (AssignmentExpressionSyntax)value; 162 | return $"{TranspileExpression(orAssign.Left, model)} |= {TranspileExpression(orAssign.Right, model)}"; 163 | case SyntaxKind.ParenthesizedExpression: 164 | var paren = (ParenthesizedExpressionSyntax)value; 165 | return $"({TranspileExpression(paren.Expression, model)})"; 166 | case SyntaxKind.ParenthesizedLambdaExpression: 167 | var parenLambda = (ParenthesizedLambdaExpressionSyntax)value; 168 | return TranspileLambdaExpression(parenLambda.ParameterList.Parameters, parenLambda, model, indent); 169 | case SyntaxKind.PostDecrementExpression: 170 | var postDec = (PostfixUnaryExpressionSyntax)value; 171 | return $"{TranspileExpression(postDec.Operand, model)} -= 1"; 172 | case SyntaxKind.PostIncrementExpression: 173 | var postInc = (PostfixUnaryExpressionSyntax)value; 174 | return $"{TranspileExpression(postInc.Operand, model)} += 1"; 175 | case SyntaxKind.PredefinedType: 176 | return GetSwiftTypeName(value, model); 177 | case SyntaxKind.PreDecrementExpression: 178 | var preDec = (PrefixUnaryExpressionSyntax)value; 179 | return $"{TranspileExpression(preDec.Operand, model)} -= 1"; 180 | case SyntaxKind.PreIncrementExpression: 181 | var preInc = (PrefixUnaryExpressionSyntax)value; 182 | return $"{TranspileExpression(preInc.Operand, model)} += 1"; 183 | case SyntaxKind.RangeExpression: 184 | var range = (RangeExpressionSyntax)value; 185 | var rangeStart = range.LeftOperand is not null ? TranspileExpression(range.LeftOperand, model) : null; 186 | var rangeEnd = range.RightOperand is not null ? TranspileExpression(range.RightOperand, model) : null; 187 | if (rangeEnd is not null) 188 | return $"{rangeStart}..<{rangeEnd}"; 189 | else 190 | return $"{rangeStart}..."; 191 | case SyntaxKind.RightShiftExpression: 192 | var rshift = (BinaryExpressionSyntax)value; 193 | return $"{TranspileExpression(rshift.Left, model)} >> {TranspileExpression(rshift.Right, model)}"; 194 | case SyntaxKind.RightShiftAssignmentExpression: 195 | var rshiftAssign = (AssignmentExpressionSyntax)value; 196 | return $"{TranspileExpression(rshiftAssign.Left, model)} >>= {TranspileExpression(rshiftAssign.Right, model)}"; 197 | case SyntaxKind.SimpleAssignmentExpression: 198 | var sae = (AssignmentExpressionSyntax)value; 199 | return $"{TranspileExpression(sae.Left, model)} = {TranspileExpression(sae.Right, model)}"; 200 | case SyntaxKind.SimpleLambdaExpression: 201 | var simpleLambda = (SimpleLambdaExpressionSyntax)value; 202 | return TranspileLambdaExpression(new[]{simpleLambda.Parameter}, simpleLambda, model, indent); 203 | case SyntaxKind.SimpleMemberAccessExpression: 204 | var sma = (MemberAccessExpressionSyntax)value; 205 | return $"{TranspileExpression(sma.Expression, model)}.{sma.Name.ToString()}"; 206 | case SyntaxKind.StringLiteralExpression: 207 | var slit = (LiteralExpressionSyntax)value; 208 | { 209 | var stext = CSStringToSwift(slit.Token.Text, slit.Token.ValueText); 210 | 211 | return stext; 212 | } 213 | case SyntaxKind.SubtractAssignmentExpression: 214 | var subAssign = (AssignmentExpressionSyntax)value; 215 | return $"{TranspileExpression(subAssign.Left, model)} -= {TranspileExpression(subAssign.Right, model)}"; 216 | case SyntaxKind.SubtractExpression: 217 | var sub = (BinaryExpressionSyntax)value; 218 | return $"{TranspileExpression(sub.Left, model)} - {TranspileExpression(sub.Right, model)}"; 219 | case SyntaxKind.ThisExpression: 220 | return "self"; 221 | case SyntaxKind.TrueLiteralExpression: 222 | return "true"; 223 | case SyntaxKind.UnaryMinusExpression: 224 | var ume = (PrefixUnaryExpressionSyntax)value; 225 | return $"-{TranspileExpression(ume.Operand, model)}"; 226 | default: 227 | Error($"Unsupported expression: {value.Kind()}"); 228 | return $"nil/*{value.Kind()}: {value.ToString().Trim()}*/"; 229 | } 230 | } 231 | 232 | string TranspileIsPattern(IsPatternExpressionSyntax value, SemanticModel model, string indent) 233 | { 234 | var exprCode = TranspileExpression(value.Expression, model, indent); 235 | switch (value.Pattern.Kind ()) { 236 | case SyntaxKind.DeclarationPattern: 237 | var dp = (DeclarationPatternSyntax)value.Pattern; 238 | var dpTypeName = GetSwiftTypeName(dp.Type, model); 239 | if (dp.Designation is SingleVariableDesignationSyntax svds) { 240 | return $"let {svds.Identifier} = {exprCode} as? {dpTypeName}"; 241 | } 242 | else { 243 | Error($"Unsupported declaration pattern: {dp.Designation.ToString()}"); 244 | return $"{exprCode}/* is {dp.Designation.ToString()}*/"; 245 | } 246 | case SyntaxKind.ConstantPattern: 247 | var cp = (ConstantPatternSyntax)value.Pattern; 248 | switch (cp.Expression.Kind()) { 249 | case SyntaxKind.TrueLiteralExpression: 250 | return $"{exprCode}"; 251 | case SyntaxKind.FalseLiteralExpression: 252 | return $"!{exprCode}"; 253 | case SyntaxKind.NullLiteralExpression: 254 | return $"{exprCode} == nil"; 255 | default: 256 | return $"{exprCode} == {TranspileExpression(cp.Expression, model)}"; 257 | } 258 | default: 259 | Error($"Unsupported pattern: {value.Pattern.Kind()}"); 260 | return $"{exprCode}/*{value.Pattern.Kind()}: {value.Pattern.ToString().Trim()}*/"; 261 | } 262 | } 263 | 264 | static readonly Regex unicodeLiteralRegex = new (@"\\u([0-9a-fA-F]{4})", RegexOptions.Compiled); 265 | 266 | string CSStringToSwift(string csStringLiteralText, string csStringLiteralValueText) { 267 | var stext = csStringLiteralText; 268 | if (stext.Length > 0 && stext[0] == '@') { 269 | stext = "\"\"\"\n" + csStringLiteralValueText + "\n\"\"\""; 270 | } 271 | stext = unicodeLiteralRegex.Replace(stext, m => { 272 | var hex = m.Groups[1].Value; 273 | var code = int.Parse(hex, System.Globalization.NumberStyles.HexNumber); 274 | return $"\\u{{{hex}}}"; 275 | }); 276 | return stext; 277 | } 278 | 279 | private string TranspileArrayCreation(ArrayCreationExpressionSyntax array, SemanticModel model, string indent) 280 | { 281 | var etypeSymbol = model.GetTypeInfo(array.Type.ElementType).Type; 282 | if (array.Type.RankSpecifiers.Count == 1 && array.Type.RankSpecifiers[0].Sizes is {Count:1} sizes) { 283 | var lengthExpr = sizes[0]; 284 | var length = TryEvaluateConstantIntExpression(lengthExpr, model, 0); 285 | if (array.Initializer is {} init) { 286 | var sb = new System.Text.StringBuilder(); 287 | sb.Append("["); 288 | var head = ""; 289 | var num = 0; 290 | foreach (var e in init.Expressions) { 291 | sb.Append(head); 292 | sb.Append(TranspileExpression(e, model, indent)); 293 | head = ", "; 294 | num++; 295 | } 296 | if (num < length) { 297 | var valueCode = GetDefaultValue(etypeSymbol); 298 | for (var i = num; i < length; i++) { 299 | sb.Append(head); 300 | sb.Append(valueCode); 301 | head = ", "; 302 | } 303 | } 304 | sb.Append("]"); 305 | return sb.ToString(); 306 | } 307 | else { 308 | var valueCode = GetDefaultValue(etypeSymbol); 309 | var lengthCode = TranspileExpression(lengthExpr, model); 310 | return $"Array(repeating: {valueCode}, count: {lengthCode})"; 311 | } 312 | } 313 | else { 314 | Error($"Unsupported array creation: {array.ToString().Trim()}"); 315 | return $"[]/*{array.ToString().Trim()}*/"; 316 | } 317 | } 318 | 319 | string TranspileElementArguments(BracketedArgumentListSyntax argList, SemanticModel model, string indent) 320 | { 321 | return string.Join(", ", argList.Arguments.Select(x => TranspileExpression(x.Expression, model, indent))); 322 | } 323 | 324 | string TranspileLambdaExpression(IEnumerable parameters, LambdaExpressionSyntax lambda, SemanticModel model, string indent) 325 | { 326 | var parametersCode = string.Join(", ", parameters.Select(x => x.Identifier.ToString())); 327 | var sb = new System.Text.StringBuilder(); 328 | sb.Append($"{{ {parametersCode} in"); 329 | if (lambda.ExpressionBody is {} expr) { 330 | var exprCode = TranspileExpression(expr, model); 331 | sb.Append($" {exprCode} }}"); 332 | } 333 | else if (lambda.Block is {} block) { 334 | using var sw = new StringWriter(); 335 | TranspileBlock(block, model, indent, sw); 336 | var blockCode = sw.ToString(); 337 | sb.Append($"\n{blockCode}{indent}}}"); 338 | } 339 | else { 340 | Error($"Unsupported lambda expression: {lambda.ToString().Trim()}"); 341 | sb.Append($" nil/*{lambda.ToString().Trim()}*/ }}"); 342 | } 343 | return sb.ToString(); 344 | } 345 | 346 | string TranspileObjectCreationExpression(ObjectCreationExpressionSyntax oc, SemanticModel model, string indent) 347 | { 348 | if (model.GetSymbolInfo(oc.Type).Symbol is ITypeSymbol ocTypeSymbol) 349 | { 350 | var ocInit = oc.Initializer; 351 | if (ocTypeSymbol.Name == "Dictionary" && ocTypeSymbol.ContainingNamespace.ToString() == "System.Collections.Generic") { 352 | var sb = new System.Text.StringBuilder(); 353 | sb.Append("["); 354 | if (ocInit is not null && ocInit.Expressions.Count > 0) 355 | { 356 | var head = ""; 357 | foreach (var kv in ocInit.Expressions) 358 | { 359 | sb.Append(head); 360 | if (kv is InitializerExpressionSyntax ie && ie.Expressions.Count == 2) { 361 | sb.Append($"{TranspileExpression(ie.Expressions[0], model)}: {TranspileExpression(ie.Expressions[1], model)}"); 362 | } 363 | else { 364 | Error($"Unsupported dictionary initializer: {kv.Kind()}"); 365 | sb.Append($"nil/*{kv.ToString().Trim()}*/"); 366 | } 367 | head = ", "; 368 | } 369 | } 370 | sb.Append("]"); 371 | return sb.ToString(); 372 | } 373 | else { 374 | var ocName = GetSwiftTypeName(ocTypeSymbol); 375 | var ocCode = oc.ArgumentList != null ? TranspileInvocation(ocName, oc, oc.ArgumentList, model, indent) : $"{ocName}()"; 376 | if (ocInit is not null && ocInit.Expressions.Count > 0) 377 | { 378 | var kw = ocTypeSymbol.IsReferenceType ? "let" : "var"; 379 | var inits = ocInit.Expressions.Select(x => $"x.{TranspileExpression(x, model)}"); 380 | var initsCode = string.Join("; ", inits); 381 | ocCode = $"{{ {kw} x = {ocCode}; {initsCode} }}()"; 382 | } 383 | return ocCode; 384 | } 385 | } 386 | else 387 | { 388 | Error($"Unable to determine type of object creation expression: {oc.Type.ToString()}"); 389 | return $"nil/*no type symbol: {oc.Type.ToString().Trim()}*/"; 390 | } 391 | } 392 | 393 | string TranspileInvocation(string exprCode, SyntaxNode invokeNode, ArgumentListSyntax argList, SemanticModel model, string indent) 394 | { 395 | if (exprCode == "nameof" && argList.Arguments.Count == 1) 396 | return NameOf(argList.Arguments[0].Expression); 397 | var method = model.GetSymbolInfo(invokeNode).Symbol as IMethodSymbol; 398 | if (method == null) { 399 | Error($"Method resolution failed: {invokeNode}"); 400 | var fargs = argList.Arguments.Select(a => TranspileExpression(a.Expression, model)).ToArray(); 401 | return $"{exprCode}({string.Join(", ", fargs)})"; 402 | } 403 | var parameters = method.Parameters; 404 | if (parameters.Length == 0) { 405 | return $"{exprCode}()"; 406 | } 407 | var args = argList.Arguments; 408 | var nparams = parameters.Length; 409 | var nargs = args.Count; 410 | var sb = new System.Text.StringBuilder(); 411 | var aindent = $"{indent} "; 412 | sb.Append(exprCode); 413 | sb.Append("("); 414 | for (var i = 0; i < nparams; i++) { 415 | var paramName = parameters[i].Name; 416 | if (i > 0) 417 | sb.Append(", "); 418 | sb.Append(paramName); 419 | sb.Append(": "); 420 | if (i < nargs) { 421 | var arg = args[i]; 422 | sb.Append(TranspileExpression(arg.Expression, model, aindent)); 423 | } else { 424 | Error("Missing argument value"); 425 | sb.Append("nil/*missing*/"); 426 | } 427 | } 428 | if (nargs > nparams) { 429 | Error("Too many arguments"); 430 | for (var i = nparams; i < nargs; i++) { 431 | var arg = args[i]; 432 | sb.Append(", "); 433 | sb.Append(TranspileExpression(arg.Expression, model, aindent)); 434 | sb.Append("/*extra*/"); 435 | } 436 | } 437 | sb.Append(")"); 438 | return sb.ToString(); 439 | } 440 | 441 | string NameOf(ExpressionSyntax value) 442 | { 443 | var name = value switch { 444 | IdentifierNameSyntax id => id.Identifier.ToString(), 445 | MemberAccessExpressionSyntax mae => mae.Name.ToString(), 446 | _ => value.ToString() 447 | }; 448 | return $"\"{name}\""; 449 | } 450 | 451 | int TryEvaluateConstantIntExpression(ExpressionSyntax expr, SemanticModel model, int defaultInt) 452 | { 453 | if (expr is LiteralExpressionSyntax lit && lit.IsKind(SyntaxKind.NumericLiteralExpression)) { 454 | return int.Parse(lit.Token.ValueText); 455 | } 456 | return defaultInt; 457 | } 458 | 459 | string GetDefaultValue(ISymbol? type) 460 | { 461 | if (type == null) { 462 | return "nil"; 463 | } 464 | switch (type.Kind) { 465 | case SymbolKind.ArrayType: 466 | return "[]"; 467 | case SymbolKind.PointerType: 468 | return "nil"; 469 | case SymbolKind.DynamicType: 470 | return "nil"; 471 | case SymbolKind.TypeParameter: 472 | return "nil"; 473 | case SymbolKind.ErrorType: 474 | return "nil"; 475 | case SymbolKind.NamedType: 476 | var ntype = (INamedTypeSymbol)type; 477 | switch (type.Name) { 478 | case nameof(System.Boolean): 479 | return "false"; 480 | case nameof(System.Byte): 481 | return "0"; 482 | case nameof(System.Char): 483 | return "\"\\0\""; 484 | case nameof(System.Double): 485 | return "0.0"; 486 | case nameof(System.Single): 487 | return "0.0"; 488 | case nameof(System.Int16): 489 | return "0"; 490 | case nameof(System.Int32): 491 | return "0"; 492 | case nameof(System.Int64): 493 | return "0"; 494 | case nameof(System.IntPtr): 495 | return "0"; 496 | case nameof(System.UInt16): 497 | return "0"; 498 | case nameof(System.UInt32): 499 | return "0"; 500 | case nameof(System.UInt64): 501 | return "0"; 502 | case nameof(System.UIntPtr): 503 | return "0"; 504 | default: 505 | if (ntype.IsReferenceType) { 506 | return "nil"; 507 | } 508 | else if (ntype.Name == "Nullable" && ntype.ContainingNamespace.Name == "System") { 509 | return "nil"; 510 | } 511 | else if (ntype.Name == "Enum" && ntype.ContainingNamespace.Name == "System") { 512 | return "0"; 513 | } 514 | else if (ntype.InstanceConstructors.FirstOrDefault(x => x.Parameters.Length == 0) is IMethodSymbol defaultCtor) { 515 | return $"{GetSwiftTypeName(type)}()"; 516 | } 517 | else { 518 | Error($"Unsupported default value for named type: {type.Name}"); 519 | return $"0/*NT:{type.Name}*/"; 520 | } 521 | } 522 | default: 523 | Error($"Unsupported default value for type {type.Kind}"); 524 | return $"nil/*T:{type.Kind}*/"; 525 | } 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /TranspileStatement.cs: -------------------------------------------------------------------------------- 1 | namespace CSharpToSwift; 2 | 3 | using System; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | 8 | partial class Transpiler { 9 | void TranspileBlock(BlockSyntax block, SemanticModel model, string indent, TextWriter w) 10 | { 11 | foreach (var stmt in block.Statements) 12 | { 13 | TranspileStatement(stmt, model, indent, w); 14 | } 15 | } 16 | 17 | void TranspileStatement(StatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 18 | { 19 | switch (stmt.Kind()) { 20 | case SyntaxKind.Block: 21 | TranspileBlock((BlockSyntax)stmt, model, indent, w); 22 | break; 23 | case SyntaxKind.BreakStatement: 24 | w.WriteLine($"{indent}break"); 25 | break; 26 | case SyntaxKind.ContinueStatement: 27 | w.WriteLine($"{indent}continue"); 28 | break; 29 | case SyntaxKind.ExpressionStatement: 30 | TranspileExpressionStatement((ExpressionStatementSyntax)stmt, model, indent, w); 31 | break; 32 | case SyntaxKind.ForStatement: 33 | TranspileForStatement((ForStatementSyntax)stmt, model, indent, w); 34 | break; 35 | case SyntaxKind.ForEachStatement: 36 | TranspileForEachStatement((ForEachStatementSyntax)stmt, model, indent, w); 37 | break; 38 | case SyntaxKind.IfStatement: 39 | TranspileIfStatement((IfStatementSyntax)stmt, model, indent, w); 40 | break; 41 | case SyntaxKind.LocalDeclarationStatement: 42 | var ld = (LocalDeclarationStatementSyntax)stmt; 43 | TranspileVariableDeclaration(ld.Declaration, model, indent, w); 44 | break; 45 | case SyntaxKind.ReturnStatement: 46 | TranspileReturnStatement((ReturnStatementSyntax)stmt, model, indent, w); 47 | break; 48 | case SyntaxKind.SwitchStatement: 49 | TranspileSwitchStatement((SwitchStatementSyntax)stmt, model, indent, w); 50 | break; 51 | case SyntaxKind.ThrowStatement: 52 | var thrw = (ThrowStatementSyntax)stmt; 53 | if (thrw.Expression is {} expr) { 54 | w.WriteLine($"{indent}throw {TranspileExpression(thrw.Expression, model)}"); 55 | } 56 | else { 57 | w.WriteLine($"{indent}throw"); 58 | } 59 | break; 60 | case SyntaxKind.TryStatement: 61 | TranspileTryStatement((TryStatementSyntax)stmt, model, indent, w); 62 | break; 63 | case SyntaxKind.UsingStatement: 64 | TranspileUsingStatement((UsingStatementSyntax)stmt, model, indent, w); 65 | break; 66 | case SyntaxKind.WhileStatement: 67 | TranspileWhileStatement((WhileStatementSyntax)stmt, model, indent, w); 68 | break; 69 | default: 70 | Error($"Unsupported statement: {stmt.Kind()}"); 71 | w.WriteLine($"{indent}/*{stmt.Kind()}: {stmt.ToString().Trim()}*/"); 72 | break; 73 | } 74 | } 75 | 76 | void TranspileExpressionStatement(ExpressionStatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 77 | { 78 | var expr = TranspileExpression(stmt.Expression, model); 79 | w.WriteLine($"{indent}{expr}"); 80 | } 81 | 82 | void TranspileForStatement(ForStatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 83 | { 84 | if (stmt.Declaration is {} decl) { 85 | TranspileVariableDeclaration(decl, model, indent, w); 86 | } 87 | var cond = stmt.Condition is not null ? TranspileExpression(stmt.Condition, model) : "true"; 88 | w.WriteLine($"{indent}while {cond} {{"); 89 | TranspileStatement(stmt.Statement, model, indent + " ", w); 90 | foreach (var incr in stmt.Incrementors) { 91 | var expr = TranspileExpression(incr, model); 92 | w.WriteLine($"{indent} {expr}"); 93 | } 94 | w.WriteLine($"{indent}}}"); 95 | } 96 | 97 | void TranspileForEachStatement(ForEachStatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 98 | { 99 | var expr = stmt.Expression; 100 | var ident = stmt.Identifier; 101 | var exprCode = TranspileExpression(expr, model); 102 | w.WriteLine($"{indent}for {ident.ValueText} in {exprCode} {{"); 103 | TranspileStatement(stmt.Statement, model, indent + " ", w); 104 | w.WriteLine($"{indent}}}"); 105 | } 106 | 107 | void TranspileIfStatement(IfStatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 108 | { 109 | var cond = TranspileExpression(stmt.Condition, model); 110 | w.WriteLine($"{indent}if {cond} {{"); 111 | TranspileStatement(stmt.Statement, model, indent + " ", w); 112 | if (stmt.Else is { } elseClause) 113 | { 114 | w.WriteLine($"{indent}}} else {{"); 115 | TranspileStatement(elseClause.Statement, model, indent + " ", w); 116 | } 117 | w.WriteLine($"{indent}}}"); 118 | } 119 | 120 | void TranspileReturnStatement(ReturnStatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 121 | { 122 | var expr = stmt.Expression; 123 | if (expr is null) 124 | w.WriteLine($"{indent}return"); 125 | else 126 | w.WriteLine($"{indent}return {TranspileExpression(expr, model)}"); 127 | } 128 | 129 | void TranspileSwitchStatement(SwitchStatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 130 | { 131 | var expr = stmt.Expression; 132 | w.WriteLine($"{indent}switch {TranspileExpression(expr, model)} {{"); 133 | foreach (var s in stmt.Sections) 134 | { 135 | var caseLabels = s.Labels.Where(x => !x.IsKind(SyntaxKind.DefaultSwitchLabel)).ToArray(); 136 | var defLabel = s.Labels.FirstOrDefault(x => x.IsKind(SyntaxKind.DefaultSwitchLabel)); 137 | if (caseLabels.Length > 0) { 138 | var labels = string.Join(", ", s.Labels.Select(x => { 139 | if (x.IsKind(SyntaxKind.DefaultSwitchLabel)) { 140 | return "default"; 141 | } else if (x.IsKind(SyntaxKind.CaseSwitchLabel)) { 142 | return TranspileExpression(((CaseSwitchLabelSyntax)x).Value, model, indent); 143 | } else { 144 | Error($"Unsupported switch label: {x}"); 145 | return x.ToString(); 146 | } 147 | })); 148 | w.WriteLine($"{indent}case {labels}:"); 149 | foreach (var cstmt in s.Statements) { 150 | TranspileStatement(cstmt, model, indent + " ", w); 151 | } 152 | } 153 | if (defLabel is not null) { 154 | w.WriteLine($"{indent}default:"); 155 | foreach (var cstmt in s.Statements) { 156 | TranspileStatement(cstmt, model, indent + " ", w); 157 | } 158 | } 159 | } 160 | w.WriteLine($"{indent}}}"); 161 | } 162 | 163 | void TranspileTryStatement(TryStatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 164 | { 165 | var block = stmt.Block; 166 | var catches = stmt.Catches; 167 | if (catches.Count > 0) { 168 | w.WriteLine($"{indent}do {{"); 169 | if (stmt.Finally is { } fin) { 170 | w.WriteLine($"{indent} defer {{"); 171 | TranspileBlock(fin.Block, model, indent + " ", w); 172 | w.WriteLine($"{indent} }}"); 173 | } 174 | TranspileBlock(block, model, indent + " ", w); 175 | foreach (var catchClause in catches) { 176 | w.Write($"{indent}}} catch "); 177 | if (catchClause.Declaration is { } decl) { 178 | w.Write (GetSwiftTypeName(decl.Type, model)); 179 | } 180 | if (catchClause.Filter is { } filter) { 181 | var cond = TranspileExpression(filter.FilterExpression, model); 182 | w.WriteLine($"where {cond}"); 183 | } 184 | w.WriteLine($" {{"); 185 | TranspileBlock(catchClause.Block, model, indent + " ", w); 186 | w.WriteLine($"{indent}}}"); 187 | } 188 | } 189 | else { 190 | w.WriteLine($"{indent}{{"); 191 | if (stmt.Finally is { } fin) { 192 | w.WriteLine($"{indent} defer {{"); 193 | TranspileBlock(fin.Block, model, indent + " ", w); 194 | w.WriteLine($"{indent} }}"); 195 | } 196 | TranspileBlock(block, model, indent + " ", w); 197 | w.WriteLine($"{indent}}}"); 198 | } 199 | } 200 | 201 | void TranspileUsingStatement(UsingStatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 202 | { 203 | w.WriteLine($"{indent}{{"); 204 | if (stmt.Expression is {} expr) { 205 | var exprCode = TranspileExpression(expr, model); 206 | w.WriteLine($"{indent} let using_ = {exprCode}"); 207 | } 208 | else if (stmt.Declaration is {} decl) { 209 | TranspileVariableDeclaration(decl, model, indent + " ", w); 210 | } 211 | else { 212 | Error($"Unsupported using statement"); 213 | } 214 | w.WriteLine($"{indent} defer {{"); 215 | if (stmt.Expression is not null) { 216 | w.WriteLine($"{indent} using_.Dispose()"); 217 | } 218 | else if (stmt.Declaration is not null) { 219 | foreach (var v in stmt.Declaration.Variables) { 220 | var name = v.Identifier.ValueText; 221 | w.WriteLine($"{indent} {name}.Dispose()"); 222 | } 223 | } 224 | w.WriteLine($"{indent} }}"); 225 | TranspileStatement(stmt.Statement, model, indent + " ", w); 226 | w.WriteLine($"{indent}}}"); 227 | } 228 | 229 | void TranspileVariableDeclaration(VariableDeclarationSyntax decl, SemanticModel model, string indent, TextWriter w) 230 | { 231 | var declType = model.GetTypeInfo(decl.Type).Type; 232 | var declTypeName = GetSwiftTypeName(declType); 233 | foreach (var v in decl.Variables) 234 | { 235 | var vn = v.Identifier.ToString(); 236 | var initCode = v.Initializer is not null ? TranspileExpression(v.Initializer.Value, model) : GetDefaultValue(declType); 237 | w.WriteLine($"{indent}var {vn}: {declTypeName} = {initCode}"); 238 | } 239 | } 240 | 241 | void TranspileWhileStatement(WhileStatementSyntax stmt, SemanticModel model, string indent, TextWriter w) 242 | { 243 | var cond = TranspileExpression(stmt.Condition, model); 244 | w.WriteLine($"{indent}while {cond} {{"); 245 | TranspileStatement(stmt.Statement, model, indent + " ", w); 246 | w.WriteLine($"{indent}}}"); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /TranspileType.cs: -------------------------------------------------------------------------------- 1 | namespace CSharpToSwift; 2 | 3 | using System; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | 8 | partial class Transpiler { 9 | void TranspileClass(string swiftName, ClassDeclarationSyntax node, INamedTypeSymbol symbol, SemanticModel model, string indent, TextWriter w) 10 | { 11 | w.Write($"{indent}class {swiftName}"); 12 | var head = " : "; 13 | if (symbol.BaseType is {} baseType && !(baseType.Name == "Object" && baseType.ContainingNamespace.Name == "System")) { 14 | var baseSwiftName = GetSwiftTypeName(baseType); 15 | w.Write($"{head}{baseSwiftName}"); 16 | head = ", "; 17 | } 18 | foreach (var i in symbol.Interfaces) { 19 | var baseSwiftName = GetSwiftTypeName(i); 20 | w.Write($"{head}{baseSwiftName}"); 21 | head = ", "; 22 | } 23 | w.WriteLine($" {{"); 24 | foreach (var member in node.Members) { 25 | TranspileClassOrStructMember(member, swiftName, node, symbol, model, indent + " ", w, requireMethodBody: true); 26 | } 27 | w.WriteLine($"{indent}}}"); 28 | } 29 | 30 | void TranspileInterface(string swiftName, InterfaceDeclarationSyntax node, INamedTypeSymbol symbol, SemanticModel model, string indent, TextWriter w) 31 | { 32 | w.Write($"{indent}protocol {swiftName}"); 33 | var head = " : "; 34 | foreach (var i in symbol.Interfaces) { 35 | var baseSwiftName = GetSwiftTypeName(i); 36 | w.Write($"{head}{baseSwiftName}"); 37 | head = ", "; 38 | } 39 | w.WriteLine($" {{"); 40 | foreach (var member in node.Members) { 41 | TranspileClassOrStructMember(member, swiftName, node, symbol, model, indent + " ", w, requireMethodBody: false); 42 | } 43 | w.WriteLine($"{indent}}}"); 44 | } 45 | 46 | void TranspileStruct(string swiftName, StructDeclarationSyntax node, INamedTypeSymbol symbol, SemanticModel model, string indent, TextWriter w) 47 | { 48 | w.Write($"{indent}struct {swiftName}"); 49 | var head = " : "; 50 | foreach (var i in symbol.Interfaces) { 51 | var baseSwiftName = GetSwiftTypeName(i); 52 | w.Write($"{head}{baseSwiftName}"); 53 | head = ", "; 54 | } 55 | w.WriteLine($" {{"); 56 | foreach (var member in node.Members) { 57 | TranspileClassOrStructMember(member, swiftName, node, symbol, model, indent + " ", w, requireMethodBody: true); 58 | } 59 | w.WriteLine($"{indent}}}"); 60 | } 61 | 62 | void TranspileClassOrStructMember(MemberDeclarationSyntax member, string typeName, TypeDeclarationSyntax node, INamedTypeSymbol typeSymbol, SemanticModel model, string indent, TextWriter w, bool requireMethodBody) 63 | { 64 | switch (member.Kind ()) { 65 | case SyntaxKind.ClassDeclaration: 66 | var classDecl = (ClassDeclarationSyntax)member; 67 | if (model.GetDeclaredSymbol(classDecl) is INamedTypeSymbol classSymbol) { 68 | var classSwiftName = GetSwiftTypeName(classSymbol); 69 | TranspileClass(classSwiftName, classDecl, classSymbol, model, indent, w); 70 | } 71 | else { 72 | Error($"Unable to get symbol for class: {classDecl.Identifier.Text}"); 73 | w.WriteLine($"{indent}/*{classDecl.ToString().Trim()}*/"); 74 | } 75 | break; 76 | case SyntaxKind.ConstructorDeclaration: 77 | TranspileCtor((ConstructorDeclarationSyntax)member, typeSymbol, model, indent, w, requireMethodBody: requireMethodBody); 78 | break; 79 | case SyntaxKind.FieldDeclaration: 80 | TranspileField((FieldDeclarationSyntax)member, typeSymbol, model, indent, w); 81 | break; 82 | case SyntaxKind.MethodDeclaration: 83 | TranspileMethod((MethodDeclarationSyntax)member, typeSymbol, model, indent, w, requireMethodBody: requireMethodBody); 84 | break; 85 | case SyntaxKind.PropertyDeclaration: 86 | TranspileProperty((PropertyDeclarationSyntax)member, typeSymbol, model, indent, w, requireMethodBody: requireMethodBody); 87 | break; 88 | case SyntaxKind.StructDeclaration: 89 | var structDecl = (StructDeclarationSyntax)member; 90 | if (model.GetDeclaredSymbol(structDecl) is INamedTypeSymbol structSymbol) { 91 | var structSwiftName = GetSwiftTypeName(structSymbol); 92 | TranspileStruct(structSwiftName, structDecl, structSymbol, model, indent, w); 93 | } 94 | else { 95 | Error($"Unable to get symbol for struct: {structDecl.Identifier.Text}"); 96 | w.WriteLine($"{indent}/*{structDecl.ToString().Trim()}*/"); 97 | } 98 | break; 99 | default: 100 | Error($"Unsupported member: {member.Kind()}"); 101 | w.WriteLine($" /*{member.Kind()}: {member.ToString().Trim()}*/"); 102 | break; 103 | } 104 | } 105 | 106 | void TranspileField(FieldDeclarationSyntax field, INamedTypeSymbol containerTypeSymbol, SemanticModel model, string indent, TextWriter w) 107 | { 108 | var docs = GetDocs(field); 109 | var type = model.GetSymbolInfo(field.Declaration.Type).Symbol; 110 | 111 | var ftypeName = GetSwiftTypeName(type); 112 | var isReadOnly = field.Modifiers.Any(x => x.IsKind(SyntaxKind.ReadOnlyKeyword)); 113 | var isStatic = field.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword)); 114 | var decl = isReadOnly ? (isStatic ? "static let" : "let") : (isStatic ? "static var" : "var"); 115 | 116 | foreach (var v in field.Declaration.Variables) 117 | { 118 | var fieldSymbol = model.GetDeclaredSymbol(v); 119 | var acc = GetAccessLevelModifier(fieldSymbol); 120 | var vn = v.Identifier.ToString(); 121 | var initCode = v.Initializer is not null ? TranspileExpression(v.Initializer.Value, model, $"{indent} ") : null; 122 | if (initCode is null && !isReadOnly) 123 | initCode = GetDefaultValue(type); 124 | var typeSuffix = "";//TODO: Enable null checking. initCode == "nil" ? "?" : ""; 125 | if (initCode is not null) 126 | initCode = " = " + initCode; 127 | if (docs.Length > 0) 128 | w.WriteLine($"{indent}/// {docs}"); 129 | w.WriteLine($"{indent}{acc}{decl} {vn}: {ftypeName}{typeSuffix}{initCode}"); 130 | } 131 | } 132 | 133 | void TranspileCtor(ConstructorDeclarationSyntax ctor, INamedTypeSymbol containerTypeSymbol, SemanticModel model, string indent, TextWriter w, bool requireMethodBody) 134 | { 135 | var docs = GetDocs(ctor); 136 | if (docs.Length > 0) 137 | w.WriteLine($"{indent}/// {docs}"); 138 | var methodSymbol = model.GetDeclaredSymbol(ctor); 139 | var acc = GetAccessLevelModifier(methodSymbol); 140 | var isStatic = ctor.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword)); 141 | var slotType = isStatic ? "static " : ""; 142 | w.Write($"{indent}{acc}{slotType}init("); 143 | TranspileParams(ctor.ParameterList, model, w); 144 | if (ctor.Body is null && !requireMethodBody) { 145 | w.WriteLine($")"); 146 | } 147 | else { 148 | w.WriteLine($") {{"); 149 | if (ctor.Body is {} block) { 150 | TranspileBlock(block, model, $"{indent} ", w); 151 | } 152 | w.WriteLine($"{indent}}}"); 153 | } 154 | } 155 | 156 | void TranspileMethod(MethodDeclarationSyntax method, INamedTypeSymbol containerTypeSymbol, SemanticModel model, string indent, TextWriter w, bool requireMethodBody) 157 | { 158 | var docs = GetDocs(method); 159 | if (docs.Length > 0) 160 | w.WriteLine($"{indent}/// {docs}"); 161 | var returnType = model.GetSymbolInfo(method.ReturnType).Symbol; 162 | var isVoid = IsTypeVoid(returnType); 163 | var returnTypeCode = isVoid ? "" : $" -> {GetSwiftTypeName(returnType)}"; 164 | var methodSymbol = model.GetDeclaredSymbol(method); 165 | var acc = GetAccessLevelModifier(methodSymbol); 166 | var isStatic = method.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword)); 167 | var isOverride = method.Modifiers.Any(x => x.IsKind(SyntaxKind.OverrideKeyword)); 168 | var isSealed = method.Modifiers.Any(x => x.IsKind(SyntaxKind.SealedKeyword)); 169 | var isAbstract = method.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)); 170 | var isVirtual = method.Modifiers.Any(x => x.IsKind(SyntaxKind.VirtualKeyword)); 171 | if (isAbstract) 172 | { 173 | // Warning("Abstract methods are not supported"); 174 | } 175 | // acc = acc + $"/*{method.Modifiers}*/"; 176 | var slotType = isStatic ? "static " : (isOverride ? "override " : (isAbstract ? "/*abstract*/ " : (isVirtual ? "" : (method.Body is not null ? "final " : "")))); 177 | w.Write($"{indent}{acc}{slotType}func {method.Identifier.ToString()}("); 178 | TranspileParams(method.ParameterList, model, w); 179 | w.Write($"){returnTypeCode}"); 180 | if (method.Body is null && !requireMethodBody) { 181 | w.WriteLine(); 182 | } 183 | else { 184 | w.WriteLine($" {{"); 185 | if (method.Body is {} block) { 186 | TranspileBlock(block, model, $"{indent} ", w); 187 | } 188 | w.WriteLine($"{indent}}}"); 189 | } 190 | } 191 | 192 | private void TranspileParams(ParameterListSyntax parameterList, SemanticModel model, TextWriter w) 193 | { 194 | var head = ""; 195 | foreach (var p in parameterList.Parameters) 196 | { 197 | var pname = p.Identifier.ToString(); 198 | if (p.Type is null) { 199 | Error($"Parameter has no type: {pname}"); 200 | w.Write($"{head}{pname}: Int/*Error: not type*/"); 201 | } 202 | else { 203 | var ptypeSymbol = model.GetSymbolInfo(p.Type).Symbol; 204 | var isArray = IsTypeArray(ptypeSymbol); 205 | var ptypeName = GetSwiftTypeName(ptypeSymbol); 206 | var refMod = isArray ? "inout " : ""; 207 | w.Write($"{head}{pname}: {refMod}{ptypeName}"); 208 | } 209 | head = ", "; 210 | } 211 | } 212 | 213 | void TranspileProperty(PropertyDeclarationSyntax prop, INamedTypeSymbol containerTypeSymbol, SemanticModel model, string indent, TextWriter w, bool requireMethodBody) 214 | { 215 | var docs = GetDocs(prop); 216 | if (docs.Length > 0) 217 | w.WriteLine($"{indent}/// {docs}"); 218 | var returnType = model.GetSymbolInfo(prop.Type).Symbol; 219 | string slotType = GetSlotTypeModifier(prop); 220 | var vn = prop.Identifier.ToString(); 221 | var initCode = prop.Initializer is not null ? TranspileExpression(prop.Initializer.Value, model) : null; 222 | if (initCode is not null) 223 | initCode = " = " + initCode; 224 | w.WriteLine($"{indent}{slotType}var {vn}: {GetSwiftTypeName(returnType)}{initCode} {{"); 225 | if (prop.AccessorList is { } alist) 226 | { 227 | foreach (var accessor in alist.Accessors) 228 | { 229 | var accLevel = GetAccessLevelModifier(accessor, model); 230 | w.Write($"{indent} {accessor.Keyword}"); 231 | if (accessor.Body is null && accessor.ExpressionBody is null && !requireMethodBody) { 232 | w.WriteLine(); 233 | } 234 | else { 235 | w.WriteLine($" {{"); 236 | if (accessor.Body is {} block) { 237 | TranspileBlock(block, model, $"{indent} ", w); 238 | } 239 | else if (accessor.ExpressionBody is {} ebody) { 240 | var eCode = TranspileExpression(ebody.Expression, model, $"{indent} "); 241 | w.WriteLine($"{indent} {eCode}"); 242 | } 243 | w.WriteLine($"{indent} }}"); 244 | } 245 | } 246 | } 247 | else if (prop.ExpressionBody is {} ebody) { 248 | var eCode = TranspileExpression(ebody.Expression, model, $"{indent} "); 249 | w.WriteLine($"{indent} get {{ {eCode} }}"); 250 | } 251 | else { 252 | Error($"Property has no accessors: {vn}"); 253 | w.WriteLine($"{indent} // No accessors"); 254 | } 255 | w.WriteLine($"{indent}}}"); 256 | } 257 | 258 | string GetSwiftTypeName(CSharpSyntaxNode type, SemanticModel model) 259 | { 260 | var typeSymbol = model.GetSymbolInfo(type).Symbol; 261 | return GetSwiftTypeName(typeSymbol); 262 | } 263 | 264 | string GetSwiftTypeName(ISymbol? s) 265 | { 266 | if (s == null) { 267 | Error($"No symbol type provided"); 268 | return "AnyObject/*no symbol*/"; 269 | } 270 | else if (s is IArrayTypeSymbol ats) { 271 | return $"[{GetSwiftTypeName(ats.ElementType)}]"; 272 | } 273 | else if (s is INamedTypeSymbol nts) { 274 | var name = nts.Name; 275 | switch (name) { 276 | case nameof(System.Boolean): 277 | return "Bool"; 278 | case nameof(System.Byte): 279 | return "UInt8"; 280 | case nameof(System.Char): 281 | return "Character"; 282 | case nameof(System.Int32): 283 | return "Int"; 284 | case nameof(System.IntPtr): 285 | return "Int"; 286 | case nameof(System.Object): 287 | return "AnyObject"; 288 | case nameof(System.Single): 289 | return "Float"; 290 | default: 291 | if (string.IsNullOrEmpty(name)) { 292 | Error($"No name for symbol: {s.GetType()}"); 293 | return "AnyObject/*no name*/"; 294 | } 295 | else if (name == "Nullable" && s.ContainingNamespace.Name == "System") { 296 | return GetSwiftTypeName(nts.TypeArguments.First()) + "?"; 297 | } 298 | return name; 299 | } 300 | } 301 | else if (s is ITypeParameterSymbol tps) { 302 | return tps.Name; 303 | } 304 | else { 305 | Error($"Unsupported type symbol: {s.GetType()}"); 306 | return $"AnyObject/*{s}*/"; 307 | } 308 | } 309 | 310 | bool IsTypeVoid(ISymbol? typeSymbol) 311 | { 312 | return typeSymbol is null || (typeSymbol.Name == "Void" && typeSymbol.ContainingNamespace.Name == "System"); 313 | } 314 | 315 | static bool IsTypeArray(ISymbol? typeSymbol) 316 | { 317 | return typeSymbol is IArrayTypeSymbol; 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /Transpiler.cs: -------------------------------------------------------------------------------- 1 | namespace CSharpToSwift; 2 | 3 | using System; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | using Microsoft.CodeAnalysis.MSBuild; 8 | 9 | partial class Transpiler 10 | { 11 | readonly string projectFilePath; 12 | readonly string swiftPackageName; 13 | readonly string swiftPackageDir; 14 | readonly string sourcesDir; 15 | readonly MSBuildWorkspace workspace = MSBuildWorkspace.Create(); 16 | public Transpiler(string projectFilePath, string outputDir) 17 | { 18 | this.projectFilePath = Path.GetFullPath(projectFilePath); 19 | this.swiftPackageName = Path.GetFileNameWithoutExtension(projectFilePath); 20 | this.swiftPackageDir = outputDir; 21 | this.sourcesDir = outputDir; 22 | } 23 | static Transpiler() 24 | { 25 | Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults(); 26 | var _ = typeof(Microsoft.CodeAnalysis.CSharp.Formatting.CSharpFormattingOptions); 27 | } 28 | readonly Dictionary errorCounts = new Dictionary (); 29 | void Error(string message) 30 | { 31 | if (!errorCounts.ContainsKey(message)) { 32 | errorCounts[message] = 0; 33 | // Console.ForegroundColor = ConsoleColor.Red; 34 | // Console.WriteLine(message); 35 | // Console.ResetColor(); 36 | } 37 | errorCounts[message]++; 38 | } 39 | void Info(string message) 40 | { 41 | Console.ForegroundColor = ConsoleColor.Green; 42 | Console.WriteLine(message); 43 | Console.ResetColor(); 44 | } 45 | 46 | public async Task TranspileAsync() 47 | { 48 | Directory.CreateDirectory(sourcesDir); 49 | var projectFileName = Path.GetFileName(projectFilePath); 50 | Info($"Loading project {projectFileName}..."); 51 | var project = await workspace.OpenProjectAsync(projectFilePath); 52 | var projectLoadErrors = workspace.Diagnostics.Where(x => x.Kind == WorkspaceDiagnosticKind.Failure); 53 | foreach (var d in projectLoadErrors) { 54 | Error(d.Message); 55 | } 56 | if (projectLoadErrors.Any()) { 57 | Console.WriteLine($"Failed to load project {projectFileName}"); 58 | return; 59 | } 60 | Info($"Analyzing project {project.Name}..."); 61 | var compilation = await project.GetCompilationAsync(); 62 | if (compilation is null) { 63 | Error("Failed to get compilation"); 64 | return; 65 | } 66 | var compErrors = compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error).ToList(); 67 | if (compErrors.Count > 0) { 68 | foreach (var d in compErrors) { 69 | Error(d.ToString()); 70 | } 71 | return; 72 | } 73 | 74 | Info($"Transpiling..."); 75 | var types = new List<(MemberDeclarationSyntax Syntax, SemanticModel Model)>(); 76 | await GetTypeDeclarationsAsync (compilation, types); 77 | 78 | // var outputDir = System.IO.Path.GetDirectoryName(projectFilePath); 79 | TextWriter NewSwiftWriter(string swiftName) { 80 | var fileName = $"{swiftName}.swift"; 81 | var filePath = System.IO.Path.Combine(sourcesDir, fileName); 82 | var w = new System.IO.StreamWriter(filePath); 83 | w.WriteLine($"// This file was generated from {Path.GetFileName(projectFileName)} by CSharpToSwift version {typeof(Transpiler).Assembly.GetName().Version}"); 84 | return w; 85 | } 86 | using (var pw = new StreamWriter(Path.Combine(swiftPackageDir, "Package.swift"))) { 87 | pw.WriteLine($"// swift-tools-version: 5.6"); 88 | pw.WriteLine(); 89 | pw.WriteLine($"import PackageDescription"); 90 | pw.WriteLine(); 91 | pw.WriteLine($"let package = Package("); 92 | pw.WriteLine($" name: \"{swiftPackageName}\","); 93 | pw.WriteLine($" products: ["); 94 | pw.WriteLine($" .library(name: \"{swiftPackageName}\","); 95 | pw.WriteLine($" targets: [\"{swiftPackageName}\"])"); 96 | pw.WriteLine($" ],"); 97 | pw.WriteLine($" dependencies: ["); 98 | pw.WriteLine($" ],"); 99 | pw.WriteLine($" targets: ["); 100 | pw.WriteLine($" .target(name: \"{swiftPackageName}\","); 101 | pw.WriteLine($" dependencies: []),"); 102 | pw.WriteLine($" ]"); 103 | pw.WriteLine($")"); 104 | } 105 | var swift = new StringWriter(); 106 | swift.WriteLine("// This file was generated by CSharpToSwift"); 107 | foreach (var (node, model) in types) { 108 | var symbol = (INamedTypeSymbol)model.GetDeclaredSymbol(node)!; 109 | var swiftName = GetSwiftTypeName(symbol); 110 | switch (node.Kind ()) { 111 | case SyntaxKind.ClassDeclaration: 112 | var c = (ClassDeclarationSyntax)node; 113 | using (var cw = NewSwiftWriter(swiftName)) { 114 | TranspileClass(swiftName, c, symbol, model, "", cw); 115 | } 116 | break; 117 | case SyntaxKind.InterfaceDeclaration: 118 | var intf = (InterfaceDeclarationSyntax)node; 119 | using (var sw = NewSwiftWriter(swiftName)) { 120 | TranspileInterface(swiftName, intf, symbol, model, "", sw); 121 | } 122 | break; 123 | case SyntaxKind.StructDeclaration: 124 | var s = (StructDeclarationSyntax)node; 125 | using (var sw = NewSwiftWriter(swiftName)) { 126 | TranspileStruct(swiftName, s, symbol, model, "", sw); 127 | } 128 | break; 129 | default: 130 | Error($"Unsupported type: {node.Kind()}"); 131 | break; 132 | } 133 | } 134 | // Show errors sorted by count 135 | var totalErrors = 0; 136 | foreach (var kvp in errorCounts.OrderBy(x => x.Value)) { 137 | var count = kvp.Value; 138 | totalErrors += count; 139 | Console.ForegroundColor = ConsoleColor.Red; 140 | Console.Write($"Error:"); 141 | Console.ResetColor(); 142 | Console.WriteLine($" {kvp.Key} ({count}x)"); 143 | } 144 | if (totalErrors > 0) { 145 | Console.ForegroundColor = ConsoleColor.Red; 146 | Console.WriteLine($"{totalErrors} errors"); 147 | Console.ResetColor(); 148 | } 149 | else { 150 | Console.ForegroundColor = ConsoleColor.Green; 151 | Console.WriteLine("OK"); 152 | Console.ResetColor(); 153 | } 154 | } 155 | 156 | static string GetDocs(CSharpSyntaxNode field) 157 | { 158 | var lines = 159 | field.GetLeadingTrivia() 160 | .Where(x => x.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia) || x.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)) 161 | .SelectMany(x => x.ToFullString().Split('\n')) 162 | .Select(x => 163 | x 164 | .Replace("", "") 165 | .Replace("", "") 166 | .Replace("", "**") 167 | .Replace("", "**") 168 | .Replace("///", "") 169 | .Replace("\t", " ") 170 | .Trim()) 171 | .Where(x => x.Length > 0); 172 | return string.Join(" ", lines); 173 | } 174 | 175 | static string GetSlotTypeModifier(MemberDeclarationSyntax member) 176 | { 177 | var isStatic = member.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword)); 178 | var isOverride = member.Modifiers.Any(x => x.IsKind(SyntaxKind.OverrideKeyword)); 179 | var isSealed = member.Modifiers.Any(x => x.IsKind(SyntaxKind.SealedKeyword)); 180 | var isAbstract = member.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)); 181 | var isVirtual = member.Modifiers.Any(x => x.IsKind(SyntaxKind.VirtualKeyword)); 182 | var slotType = isStatic ? "static " : (isOverride ? "override " : (isAbstract ? "open " : "")); 183 | return slotType; 184 | } 185 | 186 | static string GetAccessLevelModifier(CSharpSyntaxNode node, SemanticModel model) 187 | { 188 | var memberSymbol = model.GetSymbolInfo(node).Symbol; 189 | return GetAccessLevelModifier(memberSymbol); 190 | } 191 | 192 | static string GetAccessLevelModifier(ISymbol? memberSymbol) 193 | { 194 | // access-level-modifier: private | fileprivate | internal | public | open 195 | if (memberSymbol is null) 196 | return ""; 197 | return memberSymbol.DeclaredAccessibility switch 198 | { 199 | Accessibility.Private => "", 200 | Accessibility.Protected => "", 201 | Accessibility.Internal => "", 202 | Accessibility.Public => "", 203 | _ => "", 204 | }; 205 | } 206 | 207 | async Task GetTypeDeclarationsAsync(Compilation compilation, List<(MemberDeclarationSyntax Syntax, SemanticModel Symbol)> types) 208 | { 209 | foreach (var s in compilation.SyntaxTrees.OfType()) { 210 | // Info($"Transpiling {s.FilePath}..."); 211 | var m = compilation.GetSemanticModel(s); 212 | GetTypeDeclarations(await s.GetRootAsync().ConfigureAwait(false), m, compilation, types); 213 | } 214 | } 215 | 216 | void GetTypeDeclarations(CSharpSyntaxNode node, SemanticModel model, Compilation compilation, List<(MemberDeclarationSyntax Syntax, SemanticModel Symbol)> types) 217 | { 218 | switch (node.Kind ()) { 219 | case SyntaxKind.ClassDeclaration: 220 | var c = (ClassDeclarationSyntax)node; 221 | if (model.GetDeclaredSymbol(c) is INamedTypeSymbol ctype) { 222 | // Info($"Found class {ctype.ContainingNamespace}.{ctype.Name}"); 223 | types.Add((c, model)); 224 | } 225 | break; 226 | case SyntaxKind.StructDeclaration: 227 | var s = (StructDeclarationSyntax)node; 228 | if (model.GetDeclaredSymbol(s) is INamedTypeSymbol stype) { 229 | // Info($"Found struct {stype.ContainingNamespace}.{stype.Name}"); 230 | types.Add((s, model)); 231 | } 232 | break; 233 | case SyntaxKind.InterfaceDeclaration: 234 | var i = (InterfaceDeclarationSyntax)node; 235 | if (model.GetDeclaredSymbol(i) is INamedTypeSymbol itype) { 236 | // Info($"Found interface {itype.ContainingNamespace}.{itype.Name} {itype.GetType()}"); 237 | types.Add((i, model)); 238 | } 239 | break; 240 | case SyntaxKind.EnumDeclaration: 241 | var e = (EnumDeclarationSyntax)node; 242 | if (model.GetDeclaredSymbol(e) is INamedTypeSymbol etype) { 243 | // Info($"Found enum {etype.ContainingNamespace}.{etype.Name}"); 244 | types.Add((e, model)); 245 | } 246 | break; 247 | case SyntaxKind.DelegateDeclaration: 248 | var d = (DelegateDeclarationSyntax)node; 249 | if (model.GetDeclaredSymbol(d) is INamedTypeSymbol dtype) { 250 | // Info($"Found delegate {dtype.ContainingNamespace}.{dtype.Name}"); 251 | types.Add((d, model)); 252 | } 253 | break; 254 | case SyntaxKind.NamespaceDeclaration: 255 | var n = (NamespaceDeclarationSyntax)node; 256 | foreach (var m in n.Members) { 257 | GetTypeDeclarations(m, model, compilation, types); 258 | } 259 | break; 260 | case SyntaxKind.CompilationUnit: 261 | var cu = (CompilationUnitSyntax)node; 262 | foreach (var m in cu.Members) { 263 | GetTypeDeclarations(m, model, compilation, types); 264 | } 265 | break; 266 | default: 267 | break; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.300" 4 | } 5 | } --------------------------------------------------------------------------------