Skip to content

SplashKit Transpiler

The SplashKit Transpiler is a tool for automatically translating SplashKit C++ into other programming languages. The transpiler is designed primarily for Catch2 unit tests, but is intended to support more general SplashKit programs in the future. The transpiler uses Clang to parse C++ source files and constructs an intermediate representation of the source code. This representation is then translated into the target language. Currently, C# is the only supported target language, with each Catch2 test in C++ being converted to an xUnit test in C#.

The transpiler requires:

  • CMake
  • A C++ compiler
  • LLVM
  • Clang
  • nlohmann/json (For JSON debugging)

Install in WSL with:

Terminal window
sudo apt update
sudo apt install \
build-essential \
cmake \
clang-18 \
llvm-18-dev \
libclang-18-dev \
nlohmann-json3-dev \
libedit-dev

The transpiler is located in tools/transpiler.

  • Directorytools
    • Directorytranspiler
      • CMakeLists.txt
      • Directorysrc
        • main.cpp
        • parser.cpp
        • parser.h
        • builder.cpp
        • builder.h
        • translator.cpp
        • translator.h
        • ast.h
        • macro.h

From the transpiler directory, configure and build the project:

Terminal window
cmake -S . -B build
cmake --build build

The resulting executable is placed in the SplashKit bin directory:

bin/Transpiler

The CMake configuration automatically provides the transpiler with the include paths for the SplashKit CoreSDK, backend, Catch2, and EasyLogging++ libraries.

The transpiler accepts an optional directory containing C++ source files. If no directory is supplied, the SplashKit unit test directory is used.

Terminal window
./bin/Transpiler

The default directory is coresdk/src/test/unit_tests, but a different directory can be specified:

Terminal window
./bin/Transpiler path/to/unit_tests

The transpiler searches the selected directory for .cpp files and translates the selected unit tests. unit_test_main.cpp and logging_handling.cpp are ignored.

The translated C# files are written to:

coresdk/src/test/unit_tests/generated/csharp/

For debugging purposes, the intermediate representation of the code is also written as JSON to:

coresdk/src/test/unit_tests/generated/json/

The generated tests are standard .NET xUnit tests, so they can be run using the dotnet test command.

From the generated C# project directory:

Terminal window
dotnet test

Use --filter to run specific tests. Each Catch2 test file name is translated as an xUnit category in Pascal case. This makes it easy to run test sets:

Terminal window
dotnet test --filter "Category=SoundEffect"

Display names match Catch2 SECTION names:

Terminal window
dotnet test --filter "DisplayName~can load and create sound effects"

Catch2 tags are translated into xUnit Functions, allowing tests to be selected by function tag.

Terminal window
dotnet test --filter "Function=sound_effect"

When Clang analyses source code, it constructs an abstract syntax tree (AST), made up of nodes representing each part of the code. For example, see the Clang documentation on the statement node. Any statement in the source file is represented with a child of this class.

For translation purposes, Clang’s AST contains an unnecessary level of detail. So the transpiler uses Clang to process the file, then travels through Clang’s AST, distilling useful structures to build its own simplified custom AST, which can be used for translation to the target language.

The transpiler consists of three main stages:

  1. Parsing Catch2 macros from Clang’s AST (e.g. TEST_CASE)
  2. Building the custom AST (by navigating Clang’s AST)
  3. Translating the AST to the target language (C#)
C++ source
↓
Clang parser
↓
Custom AST
↓
C# translator
↓
C# source

parser.cpp uses Clang’s tooling library to parse each C++ source file.

The parser also uses a preprocessor callback, TestFinder, to detect Catch2 macros such as:

  • TEST_CASE
  • SECTION
  • REQUIRE
  • REQUIRE_FALSE
  • CHECK
  • CHECK_FALSE

The locations of these macros are recorded so they can be identified later when processing Clang’s AST.

builder.cpp walks through Clang’s AST, converting it into the transpiler’s own AST, defined in ast.h. The custom AST contains simpler representations of concepts such as:

  • Test cases
  • Sections
  • Statements
  • Expressions
  • Function calls
  • Variables
  • Types
  • Operators

This intermediate representation separates C++ parsing from target-language generation.

translator.cpp converts the custom AST into the target language.

For example, a C++ expression such as:

REQUIRE(value == 10);

can be represented in the custom AST as an assertion containing a binary expression. The translator can then generate the appropriate equivalent in the target language.

The C# translator module utilises xUnit as a testing tool. While xUnit has some similar structures to Catch2, the following translation decisions have been applied:

  • Each C++ file translates to an xUnit class (Pascal case)
    e.g. unit_test_sound_effect.cpp → public class UnitTestSoundEffect

  • A Catch2 TEST_CASE translates to an xUnit nested class (Pascal case)
    e.g. TEST_CASE("my test case") → public class MyTestCase

  • A Catch2 SECTION translates to an xUnit test method (Pascal case)
    e.g. SECTION("my section") → public void MySection()

  • Global statements are added to a GeneratedGlobals class, with each statement as a member. This is to avoid errors with top level statements in C#.

  • When no SECTION is provided, or the section name is empty, the name of the TEST_CASE (plus an index number) is used.
    e.g. public void MyTestCaseName1()

  • Nested SECTIONs are flattened as per below, since xUnit doesn’t support nested tests.

Nested sections in Catch2:

SECTION("Outer section")
{
// Outer statement
SECTION("Inner section")
{
// Inner statement
}
}

Converted in xUnit:

[Fact]
public void InnerSection()
{
// Outer statement
// Inner statement
}

Catch2 SECTIONs are used to represent different test paths, with setup and teardown. For example:

TEST_CASE("Vector operations")
{
vector<int> v;
SECTION("Addition")
{
// ...
}
SECTION("Subtraction")
{
// ...
}
v = {};
}

While xUnit does not natively support this, the C# translator respects the test paths. Statements occurring before and after a section are included in the generated test path, allowing the setup performed by the original Catch2 test to be preserved. For example, the translation for the above would be:

public class VectorOperations
{
[Fact]
public void Addition()
{
List<int> v = new List<int>();
// ...
v = new List<int>();
}
[Fact]
public void Subtraction()
{
List<int> v = new List<int>();
// ...
v = new List<int>();
}
}

Catch2 assertions are translated into xUnit assertions. For example:

Catch2xUnit
REQUIRE(a == b)Assert.Equal(b, a)
CHECK(a == b)Assert.Equal(b, a)
REQUIRE_FALSE(condition)Assert.False(condition)
CHECK_FALSE(condition)Assert.False(condition)

The exact generated assertion depends on the expression being tested.

The translator also converts C++ constructs into their C# equivalents where supported.

Examples include:

C++C#
std::stringstring
std::vector<T>List<T>
nullptrnull
object.method()object.Method()

The custom AST allows these translations to be performed independently of the original Clang AST.

  • C++ file names translate to xUnit Categories (Pascal case) and are applied to each xUnit test method
    e.g. unit_test_audio.cpp → [Trait("Category", "Audio")]

  • Catch2 SECTION names translate to xUnit display names e.g. SECTION("my section") → [Fact(DisplayName = "my section")]

  • Catch2 function tags translate to xUnit functions (Pascal case)
    e.g. "[sound_effect]" → [Trait("Function", "SoundEffect")]

The main source files have the following responsibilities:

FilePurpose
main.cppControls the high level translation process and selects input files
parser.cppRuns Clang and detects Catch2 macros
builder.cppConverts the Clang AST into the custom AST
ast.hDefines the custom AST
macro.hDefines recognised Catch2 macros
translator.cppGenerates C# from the custom AST

To support a new C++ construct, first determine which Clang AST node represents it. For example, return statements are represented by ReturnStmt. When the builder encounters this Clang node, it should use it to construct a corresponding node in the custom AST.

  1. Add a new node to ast.h.
  2. Add Clang AST handling in builder.cpp, to convert the Clang node to the new custom node.
  3. Add translation logic in translator.cpp, writing the new node in the target language.

The custom AST should remain independent of target language syntax, so that the same node can be used by other languages in the future.

To support another Catch2 macro:

  1. Add a new MacroKind to macro.h.
  2. Detect the macro in TestFinder::MacroExpands() in parser.cpp.
  3. Add handling for the macro when building the custom AST.
  4. Add the appropriate translation in translator.cpp.

A new target language should use the existing parser and custom AST.

For example:

C++ source
↓
Clang
↓
Custom AST
↙ ↘
C# New language
Translator Translator

First, add a new class to translator.h, inheriting from Translator. For example, class PythonTranslator : Translator. The new translator should traverse the custom AST and generate the syntax required by the new language.

The parser and AST builder should not contain target-specific code. This allows multiple target languages to share the same C++ parsing and intermediate representation. For example, the custom AST can represent a C++ vector<string> without specifying its final syntax. The C# translator can convert it to List<string>, while another translator could map it to the equivalent type in its own language.

To run the new translator, modify main.cpp to instantiate it as per the C# translator.