Segfault in AsyncCallback.lf
[CI failed in a branch that fixed a formatting bug](https://github.com/lf-lang/lingua-franca/actions/runs/3651441353/jobs/6168663352). The changes in this branch were unrelated to C++ code generator and runtime.

Exit code -1073741819, access violation.

Debug log:
```
    +---------------------------------------------------------------------------+
    Failed: src\concurrent\AsyncCallback.lf
    -----------------------------------------------------------------------------
    Reason: Test did not pass. Exit code: -1073741819
    Compilation output:
    Generating code for: file:/D:/a/lingua-franca/lingua-franca/test/Cpp/src/concurrent/AsyncCallback.lf
    ******** mode: STANDALONE
    ******** generated sources: D:\a\lingua-franca\lingua-franca\test\Cpp\src-gen\concurrent\AsyncCallback
    Path: D:\a\lingua-franca\lingua-franca\test\Cpp\src-gen\concurrent\AsyncCallback D:\a\lingua-franca\lingua-franca\test\Cpp\src-gen\concurrent\AsyncCallback
    --- Current working directory: D:\a\lingua-franca\lingua-franca\test\Cpp\build
    --- Executing command: cmake --version
    cmake version 3.25.1

    CMake suite maintained and supported by Kitware (kitware.com/cmake).
    --- Current working directory: D:\a\lingua-franca\lingua-franca\test\Cpp\build
    --- Executing command: cmake -DCMAKE_BUILD_TYPE=Test -DCMAKE_INSTALL_PREFIX=D:/a/lingua-franca/lingua-franca/test/Cpp -DCMAKE_INSTALL_BINDIR=bin/concurrent -DREACTOR_CPP_VALIDATE=ON -DREACTOR_CPP_TRACE=OFF -DREACTOR_CPP_LOG_LEVEL=4 -DLF_SRC_PKG_PATH=D:\a\lingua-franca\lingua-franca\test\Cpp D:/a/lingua-franca/lingua-franca/test/Cpp/src-gen
    -- Building for: Visual Studio 17 2022
    -- The CXX compiler identification is MSVC 19.34.31935.0
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/MSVC/14.34.31933/bin/Hostx64/x64/cl.exe - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    Not producing code coverage information since the selected compiler is no gcc
    -- Performing Test CMAKE_HAVE_LIBC_PTHREAD
    -- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Failed
    -- Looking for pthread_create in pthreads
    -- Looking for pthread_create in pthreads - not found
    -- Looking for pthread_create in pthread
    -- Looking for pthread_create in pthread - not found
    -- Found Threads: TRUE  
    -- Configuring done
    -- Generating done
    -- Build files have been written to: D:/a/lingua-franca/lingua-franca/test/Cpp/build
    --- Current working directory: D:\a\lingua-franca\lingua-franca\test\Cpp\build
    --- Executing command: cmake --build . --target AsyncCallback --parallel 2 --config Debug
    MSBuild version 17.4.0+18d5aef85 for .NET Framework
      Checking Build System
      Building Custom Rule D:/a/lingua-franca/lingua-franca/test/Cpp/src-gen/reactor-cpp-default/lib/CMakeLists.txt
      action.cc
      assert.cc
      environment.cc
      logical_time.cc
      port.cc
      reaction.cc
      reactor.cc
      scheduler.cc
      time.cc
      Generating Code...
      Auto build dll exports
         Creating library D:/a/lingua-franca/lingua-franca/test/Cpp/build/reactor-cpp-default/lib/Debug/reactor-cpp-default.lib and object D:/a/lingua-franca/lingua-franca/test/Cpp/build/reactor-cpp-default/lib/Debug/reactor-cpp-default.exp
      reactor-cpp-default.vcxproj -> D:\a\lingua-franca\lingua-franca\test\Cpp\build\reactor-cpp-default\lib\Debug\reactor-cpp-default.dll
      Building Custom Rule D:/a/lingua-franca/lingua-franca/test/Cpp/src-gen/concurrent/AsyncCallback/CMakeLists.txt
      AsyncCallback.cc
      _lf_preamble.cc
      main.cc
      Generating Code...
    D:\a\lingua-franca\lingua-franca\test\Cpp\src-gen\concurrent\AsyncCallback\__include__\time_parser.hh(160): warning C4702: unreachable code [D:\a\lingua-franca\lingua-franca\test\Cpp\build\concurrent\AsyncCallback\AsyncCallback.vcxproj]
      AsyncCallback.vcxproj -> D:\a\lingua-franca\lingua-franca\test\Cpp\build\concurrent\AsyncCallback\Debug\AsyncCallback.exe
    INFO: unreachable code [D:\a\lingua-franca\lingua-franca\test\Cpp\build\concurrent\AsyncCallback\AsyncCallback.vcxproj] [time_parser.hh:160:0]
    --- Current working directory: D:\a\lingua-franca\lingua-franca\test\Cpp\build
    --- Executing command: cmake --build . --target install --parallel 2 --config Debug
    MSBuild version 17.4.0+18d5aef85 for .NET Framework
      -- Install configuration: "Debug"
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/action.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/assert.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/config.hh.in
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/environment.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/fwd.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/impl
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/impl/action_impl.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/impl/port_impl.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/logging.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/logical_time.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/multiport.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/port.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/reaction.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/reactor-cpp.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/reactor.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/safe_vector.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/scheduler.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/semaphore.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/time.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/trace.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/value_ptr.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/include/reactor-cpp-default/reactor-cpp/config.hh
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/lib/reactor-cpp-default.lib
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/bin/concurrent/reactor-cpp-default.dll
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/share/reactor-cpp-default/cmake/reactor-cpp-defaultConfig.cmake
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/share/reactor-cpp-default/cmake/reactor-cpp-defaultConfig-debug.cmake
      -- Installing: D:/a/lingua-franca/lingua-franca/test/Cpp/bin/concurrent/AsyncCallback.exe
    SUCCESS (compiling generated C++ code)
    Generated source code is in D:\a\lingua-franca\lingua-franca\test\Cpp\src-gen\concurrent\AsyncCallback
    Compiled binary is in D:\a\lingua-franca\lingua-franca\test\Cpp\bin\concurrent

    Execution output:
    [DEBUG] Reactions sorted by index:
    [DEBUG] 0: AsyncCallback.reaction_1, __lf_Timeout.r_timer, 
    [DEBUG] 1: AsyncCallback.reaction_2, 
    [DEBUG] 2: AsyncCallback.reaction_3, 
    [DEBUG] Reactions sorted by index:
    [DEBUG] 0: AsyncCallback.reaction_1, __lf_Timeout.r_timer, 
    [DEBUG] 1: AsyncCallback.reaction_2, 
    [DEBUG] 2: AsyncCallback.reaction_3, 
    [INFO]  Starting the execution
    [DEBUG] Starting up reactor AsyncCallback
    [DEBUG] Schedule action AsyncCallback.t synchronously  with tag [2022-12-08 19:32:05.653909500, 0]
    [DEBUG] Schedule action AsyncCallback.startup synchronously  with tag [2022-12-08 19:32:05.653909500, 0]
    [DEBUG] Starting up reactor __lf_Timeout
    [DEBUG] Schedule action __lf_Timeout.timer synchronously  with tag [2022-12-08 19:32:07.653909500, 0]
    [DEBUG] Starting the scheduler...
    [DEBUG] (Worker 0) Starting
    [DEBUG] (Worker 1) Starting
    [DEBUG] (Worker 1) Wait for work
    [DEBUG] (Worker 0) do the initial scheduling
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:05.653909500, 0]
    [DEBUG] events: 2
    [DEBUG] Action AsyncCallback.t
    [DEBUG] insert reaction AsyncCallback.reaction_1 with index 0
    [DEBUG] Action AsyncCallback.startup
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 0
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_1 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] Schedule action AsyncCallback.t synchronously  with tag [2022-12-08 19:32:05.853909500, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:05.795430200, 0]
    [DEBUG] advance logical time to tag [2022-12-08 19:32:05.795430200, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:05.795826800, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    [DEBUG] Asynchronous callback 0: Assigned logical time greater than start time by 141520700 nsecs
    (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:05.795826800, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 1: Assigned logical time greater than start time by 141917300 nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:05.853909500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.t
    [DEBUG] insert reaction AsyncCallback.reaction_1 with index 0
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 0
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_1 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] Schedule action AsyncCallback.t synchronously  with tag [2022-12-08 19:32:06.053909500, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:05.956733200, 0]
    [DEBUG] advance logical time to tag [2022-12-08 19:32:05.956733200, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 2: Assigned logical time greater than start time by 302823700 nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:05.957318600, 0]
    [DEBUG] advance logical time to tag [2022-12-08 19:32:05.957318600, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 3: Assigned logical time greater than start time by 303409100 nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.053909500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.t
    [DEBUG] insert reaction AsyncCallback.reaction_1 with index 0
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 0
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_1 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] Schedule action AsyncCallback.t synchronously  with tag [2022-12-08 19:32:06.253909500, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.155740000, 0]
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.155740000, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.156079100, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    [DEBUG] Asynchronous callback 4: Assigned logical time greater than start time by 501830500 nsecs
    (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.156079100, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 5: Assigned logical time greater than start time by 502169600 nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.253909500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.t
    [DEBUG] insert reaction AsyncCallback.reaction_1 with index 0
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 0
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_1 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] Schedule action AsyncCallback.t synchronously  with tag [2022-12-08 19:32:06.453909500, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.356187600, 0]
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.356187600, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.356601600, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    [DEBUG] Asynchronous callback 6: Assigned logical time greater than start time by 702278100 nsecs
    (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.356601600, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 7: Assigned logical time greater than start time by 702692100 nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.453909500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.t
    [DEBUG] insert reaction AsyncCallback.reaction_1 with index 0
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 0
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_1 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] Schedule action AsyncCallback.t synchronously  with tag [2022-12-08 19:32:06.653909500, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.555167500, 0]
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.555167500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 8: Assigned logical time greater than start time by 901258000 nsecs[DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.555531400, 0]

    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.555531400, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 9: Assigned logical time greater than start time by 901621900 nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.653909500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.t
    [DEBUG] insert reaction AsyncCallback.reaction_1 with index 0
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 0
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_1 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] Schedule action AsyncCallback.t synchronously  with tag [2022-12-08 19:32:06.853909500, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.757686100, 0]
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.757686100, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.758082500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 10: Assigned logical time greater than start time by 1103776600 nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.758082500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 11: Assigned logical time greater than start time by 1104173000 nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.853909500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.t
    [DEBUG] insert reaction AsyncCallback.reaction_1 with index 0
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 0
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_1 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] Schedule action AsyncCallback.t synchronously  with tag [2022-12-08 19:32:07.053909500, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.955779100, 0]
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.955779100, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 12: Assigned logical time greater than start time by 1301869600 nsecs
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:06.956109700, 0]
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:06.956109700, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 13: Assigned logical time greater than start time by 1302200200 nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] advance logical time to tag [2022-12-08 19:32:07.053909500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.t
    [DEBUG] insert reaction AsyncCallback.reaction_1 with index 0
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 0
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_1 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()
    [DEBUG] Schedule action AsyncCallback.t synchronously  with tag [2022-12-08 19:32:07.253909500, 0]
    [DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:07.156927500, 0]
    [DEBUG] advance logical time to tag [2022-12-08 19:32:07.156927500, 0]
    [DEBUG] events: 1
    [DEBUG] Action AsyncCallback.a
    [DEBUG] insert reaction AsyncCallback.reaction_2 with index 1
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Process reactions of priority 1
    [DEBUG] (Scheduler) Reaction AsyncCallback.reaction_2 is ready for execution
    [DEBUG] (Worker 0) execute reaction AsyncCallback.reaction_2
    Asynchronous callback 14: Assigned logical time greater than start time by 1503018000[DEBUG] Schedule action AsyncCallback.a asynchronously  with tag [2022-12-08 19:32:07.157287300, 0]
     nsecs
    [DEBUG] (Scheduler) Scanning the reaction queue for ready reactions
    [DEBUG] (Scheduler) Reached end of reaction queue
    [DEBUG] (Scheduler) call next()

    +---------------------------------------------------------------------------+

CppTest > runConcurrentTests() FAILED
    org.opentest4j.AssertionFailedError at CppTest.java:71
```
diff --git a/org.lflang/src/org/lflang/ast/MalleableString.java b/org.lflang/src/org/lflang/ast/MalleableString.java
index 8b05623db..8475aab30 100644
--- a/org.lflang/src/org/lflang/ast/MalleableString.java
+++ b/org.lflang/src/org/lflang/ast/MalleableString.java
@@ -1,5 +1,6 @@
 package org.lflang.ast;
 
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -13,561 +14,502 @@ import java.util.function.Supplier;
 import java.util.function.ToLongFunction;
 import java.util.stream.Collector;
 import java.util.stream.Stream;
-
 import org.lflang.util.StringUtil;
 
-import com.google.common.collect.ImmutableList;
-
 /**
- * A {@code MalleableString} is an object with multiple valid textual
- * representations. These textual representations are code that may have
- * associated comments.
+ * A {@code MalleableString} is an object with multiple valid textual representations. These textual
+ * representations are code that may have associated comments.
  */
 public abstract class MalleableString {
 
-    protected List<String> comments = new ArrayList<>();
+  protected List<String> comments = new ArrayList<>();
+
+  /** Return this, indented by one more level. */
+  public MalleableString indent() {
+    return new Indented(this);
+  }
+
+  /**
+   * Change the state of this such that the badness of the supplied render result is minimized.
+   *
+   * @param providedRender A supplier of render results that should be optimized.
+   * @param badness A badness computer for render results.
+   * @param width The number of columns permitted for this, excluding indentation applied to the
+   *     whole of this.
+   * @param indentation The number of spaces used per level of indentation.
+   * @param singleLineCommentPrefix The prefix that marks the start of a single-line comment.
+   */
+  public abstract void findBestRepresentation(
+      Supplier<RenderResult> providedRender,
+      ToLongFunction<RenderResult> badness,
+      int width,
+      int indentation,
+      String singleLineCommentPrefix);
+
+  /** Return whether any representation of this contains text. */
+  public abstract boolean isEmpty();
+
+  /** Associate comments with this. */
+  public MalleableString addComments(Stream<String> comments) {
+    comments.filter(s -> !s.isBlank()).map(String::strip).forEach(this.comments::add);
+    return this;
+  }
+
+  /**
+   * Render this using {@code indentation} spaces per indentation level and {@code
+   * singleLineCommentMarker} to mark the beginnings of single-line comments.
+   */
+  public abstract RenderResult render(int indentation, String singleLineCommentMarker);
+
+  /** Return an object that can be represented as any one of the given alternatives. */
+  public static MalleableString anyOf(MalleableString... possibilities) {
+    return new Fork(possibilities);
+  }
+
+  /** Return an object that can be represented as any one of the given alternatives. */
+  public static MalleableString anyOf(String... possibilities) {
+    return new Leaf(possibilities);
+  }
+
+  /** Return an object that can be represented as any one of the given alternatives. */
+  public static MalleableString anyOf(Object... possibilities) {
+    return new Leaf(objectArrayToString(possibilities));
+  }
+
+  private static String[] objectArrayToString(Object[] objects) {
+    String[] ret = new String[objects.length];
+    for (int i = 0; i < objects.length; i++) {
+      ret[i] = String.valueOf(objects[i]);
+    }
+    return ret;
+  }
+
+  @Override
+  public String toString() {
+    List<String> temp = comments;
+    comments = List.of();
+    String ret = render(0, "").rendering;
+    comments = temp;
+    return ret;
+  }
+
+  /** Build a {@code MalleableString} in a manner analogous to the way we build {@code String}s. */
+  public static final class Builder {
+
+    private final List<MalleableString> components = new ArrayList<>();
+
+    /** Append something that can be represented as any of the given possibilities. */
+    public Builder append(MalleableString... possibilities) {
+      return insert(Function.identity(), Fork::new, possibilities, components::add);
+    }
 
-    /** Return this, indented by one more level. */
-    public MalleableString indent() {
-        return new Indented(this);
+    /** Prepend something that can be represented as any of the given possibilities. */
+    public Builder prepend(MalleableString... possibilities) {
+      return insert(Function.identity(), Fork::new, possibilities, ms -> components.add(0, ms));
     }
 
-    /**
-     * Change the state of this such that the badness of the supplied render
-     * result is minimized.
-     * @param providedRender A supplier of render results that should be
-     * optimized.
-     * @param badness A badness computer for render results.
-     * @param width The number of columns permitted for this, excluding
-     * indentation applied to the whole of this.
-     * @param indentation The number of spaces used per level of indentation.
-     * @param singleLineCommentPrefix The prefix that marks the start of a
-     * single-line comment.
-     */
-    public abstract void findBestRepresentation(
-        Supplier<RenderResult> providedRender,
-        ToLongFunction<RenderResult> badness,
-        int width,
-        int indentation,
-        String singleLineCommentPrefix
-    );
+    /** Append something that can be represented as any of the given possibilities. */
+    public Builder append(String... content) {
+      return insert(Leaf::new, Leaf::new, content, components::add);
+    }
 
-    /** Return whether any representation of this contains text. */
-    public abstract boolean isEmpty();
+    /** Append something that can be represented as any of the given possibilities. */
+    @SuppressWarnings("UnusedReturnValue")
+    public Builder append(Object... content) {
+      return append(objectArrayToString(content));
+    }
 
-    /** Associate comments with this. */
-    public MalleableString addComments(Stream<String> comments) {
-        comments.filter(s -> !s.isBlank()).map(String::strip).forEach(this.comments::add);
-        return this;
+    /** Append something that can be represented as any of the given possibilities. */
+    public MalleableString get() {
+      return new Sequence(ImmutableList.copyOf(components));
     }
 
-    /**
-     * Render this using {@code indentation} spaces per indentation level and
-     * {@code singleLineCommentMarker} to mark the beginnings of single-line comments.
-     */
-    public abstract RenderResult render(int indentation, String singleLineCommentMarker);
+    /** Append something that can be represented as any of the given possibilities. */
+    private <T> Builder insert(
+        Function<T, ? extends MalleableString> toMalleableString,
+        Function<T[], ? extends MalleableString> multiplePossibilitiesRepresenter,
+        T[] possibilities,
+        Consumer<MalleableString> addToComponents) {
+      boolean allEmpty =
+          Arrays.stream(possibilities).map(toMalleableString).allMatch(MalleableString::isEmpty);
+      if (!allEmpty) {
+        addToComponents.accept(multiplePossibilitiesRepresenter.apply(possibilities));
+      }
+      return this;
+    }
+  }
 
-    /**
-     * Return an object that can be represented as any one of the given
-     * alternatives.
-     */
-    public static MalleableString anyOf(MalleableString... possibilities) {
-        return new Fork(possibilities);
+  /** Join {@code MalleableString}s together using the given separator. */
+  public static final class Joiner implements Collector<MalleableString, Builder, MalleableString> {
+    private final Function<Builder, Builder> appendSeparator;
+    private final Function<Builder, Builder> prependPrefix;
+    private final Function<Builder, Builder> appendSuffix;
+
+    /** Join strings using {@code separator}. */
+    public Joiner(String separator) {
+      this(MalleableString.anyOf(separator));
+    }
+
+    /** Join strings using {@code separator}. */
+    public Joiner(MalleableString separator) {
+      this(separator, MalleableString.anyOf(""), MalleableString.anyOf(""));
     }
 
     /**
-     * Return an object that can be represented as any one of the given
-     * alternatives.
+     * Join strings using {@code separator} and delimit the result with {@code prefix} and {@code
+     * suffix}.
      */
-    public static MalleableString anyOf(String... possibilities) {
-        return new Leaf(possibilities);
+    public Joiner(MalleableString separator, MalleableString prefix, MalleableString suffix) {
+      this.appendSeparator =
+          builder -> builder.components.isEmpty() ? builder : builder.append(separator);
+      this.prependPrefix = builder -> builder.prepend(prefix);
+      this.appendSuffix = builder -> builder.append(suffix);
     }
 
     /**
-     * Return an object that can be represented as any one of the given
-     * alternatives.
+     * Join strings using {@code separator} and delimit the result with {@code prefix} and {@code
+     * suffix}.
      */
-    public static MalleableString anyOf(Object... possibilities) {
-        return new Leaf(objectArrayToString(possibilities));
+    public Joiner(String separator, String prefix, String suffix) {
+      this(
+          MalleableString.anyOf(separator),
+          MalleableString.anyOf(prefix),
+          MalleableString.anyOf(suffix));
     }
 
-    private static String[] objectArrayToString(Object[] objects) {
-        String[] ret = new String[objects.length];
-        for (int i = 0; i < objects.length; i++) {
-            ret[i] = String.valueOf(objects[i]);
-        }
-        return ret;
+    @Override
+    public Supplier<Builder> supplier() {
+      return Builder::new;
     }
 
-    /**
-     * Build a {@code MalleableString} in a manner analogous to the way we build
-     * {@code String}s.
-     */
-    public static final class Builder {
+    @Override
+    public BiConsumer<Builder, MalleableString> accumulator() {
+      return (b, ms) -> appendSeparator.apply(b).append(ms);
+    }
 
-        private final List<MalleableString> components = new ArrayList<>();
+    @Override
+    public BinaryOperator<Builder> combiner() {
+      return (builder0, builder1) -> {
+        builder1.components.forEach(ms -> accumulator().accept(builder0, ms));
+        return builder0;
+      };
+    }
 
-        /**
-         * Append something that can be represented as any of the given
-         * possibilities.
-         */
-        public Builder append(MalleableString... possibilities) {
-            return insert(Function.identity(), Fork::new, possibilities, components::add);
-        }
+    @Override
+    public Function<Builder, MalleableString> finisher() {
+      return ((Function<Builder, MalleableString>) Builder::get)
+          .compose(appendSuffix)
+          .compose(prependPrefix);
+    }
 
-        /**
-         * Prepend something that can be represented as any of the given
-         * possibilities.
-         */
-        public Builder prepend(MalleableString... possibilities) {
-            return insert(
-                Function.identity(),
-                Fork::new,
-                possibilities,
-                ms -> components.add(0, ms)
-            );
-        }
+    @Override
+    public Set<Characteristics> characteristics() {
+      return Set.of();
+    }
+  }
+
+  /** The result of rendering a {@code MalleableString}. */
+  public record RenderResult(
+      Stream<String> unplacedComments, String rendering, int levelsOfCommentDisplacement) {
+    private RenderResult with(Stream<String> moreUnplacedComments) {
+      return new RenderResult(
+          Stream.concat(moreUnplacedComments, unplacedComments),
+          rendering,
+          levelsOfCommentDisplacement);
+    }
+  }
 
-        /**
-         * Append something that can be represented as any of the given
-         * possibilities.
-         */
-        public Builder append(String... content) {
-            return insert(Leaf::new, Leaf::new, content, components::add);
-        }
+  /** Represent a list of items that should be rendered in sequence. */
+  private static final class Sequence extends MalleableString {
 
-        /**
-         * Append something that can be represented as any of the given
-         * possibilities.
-         */
-        @SuppressWarnings("UnusedReturnValue")
-        public Builder append(Object... content) {
-            return append(objectArrayToString(content));
-        }
+    private final ImmutableList<MalleableString> components;
 
-        /**
-         * Append something that can be represented as any of the given
-         * possibilities.
-         */
-        public MalleableString get() {
-            return new Sequence(ImmutableList.copyOf(components));
-        }
+    private Sequence(ImmutableList<MalleableString> components) {
+      this.components = components;
+    }
 
-        /**
-         * Append something that can be represented as any of the given
-         * possibilities.
-         */
-        private <T> Builder insert(
-            Function<T, ? extends MalleableString> toMalleableString,
-            Function<T[], ? extends MalleableString> multiplePossibilitiesRepresenter,
-            T[] possibilities,
-            Consumer<MalleableString> addToComponents
-        ) {
-            boolean allEmpty = Arrays.stream(possibilities)
-                .map(toMalleableString)
-                .allMatch(MalleableString::isEmpty);
-            if (!allEmpty) {
-                addToComponents.accept(multiplePossibilitiesRepresenter.apply(possibilities));
-            }
-            return this;
-        }
+    private boolean keepCommentsOnSameLine = false;
+    private int width = 0;
+
+    @Override
+    public RenderResult render(int indentation, String singleLineCommentPrefix) {
+      List<RenderResult> componentRenderings =
+          components.stream()
+              .map(malleableString -> malleableString.render(indentation, singleLineCommentPrefix))
+              .toList();
+      List<List<String>> commentsFromChildren =
+          componentRenderings.stream().map(it -> it.unplacedComments).map(Stream::toList).toList();
+      List<String> stringComponents =
+          componentRenderings.stream()
+              .map(it -> it.rendering)
+              .map(StringUtil::normalizeEol)
+              .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
+      List<String> commentsThatCouldNotBeHandledHere = new ArrayList<>();
+      int startColumn = inlineCommentStartColumn(stringComponents, commentsFromChildren);
+      int numCommentsDisplacedHere = 0;
+      if (commentsFromChildren.stream().anyMatch(s -> !s.isEmpty())) {
+        for (int i = 0; i < commentsFromChildren.size(); i++) {
+          if (!FormattingUtils.placeComment(
+              commentsFromChildren.get(i),
+              stringComponents,
+              i,
+              width,
+              keepCommentsOnSameLine,
+              singleLineCommentPrefix,
+              startColumn)) {
+            commentsThatCouldNotBeHandledHere.addAll(commentsFromChildren.get(i));
+            if (i != 0) numCommentsDisplacedHere++;
+          }
+        }
+      }
+      return new RenderResult(
+          Stream.concat(this.comments.stream(), commentsThatCouldNotBeHandledHere.stream()),
+          String.join("", stringComponents),
+          componentRenderings.stream().mapToInt(RenderResult::levelsOfCommentDisplacement).sum()
+              + numCommentsDisplacedHere);
     }
 
     /**
-     * Join {@code MalleableString}s together using the given separator.
+     * Return the ideal starting column of an aligned comment, or 0 if comments should not be
+     * aligned.
+     *
+     * @param stringComponents The non-comment components of the sequence.
+     * @param comments The comments that will need to be incorporated into the sequence's
+     *     representation.
      */
-    public static final class Joiner implements Collector<
-        MalleableString,
-        Builder,
-        MalleableString
-    > {
-        private final Function<Builder, Builder> appendSeparator;
-        private final Function<Builder, Builder> prependPrefix;
-        private final Function<Builder, Builder> appendSuffix;
-
-        /** Join strings using {@code separator}. */
-        public Joiner(String separator) {
-            this(MalleableString.anyOf(separator));
-        }
-
-        /** Join strings using {@code separator}. */
-        public Joiner(MalleableString separator) {
-            this(separator, MalleableString.anyOf(""), MalleableString.anyOf(""));
-        }
-
-        /**
-         * Join strings using {@code separator} and delimit the result with
-         * {@code prefix} and {@code suffix}.
-         */
-        public Joiner(MalleableString separator, MalleableString prefix, MalleableString suffix) {
-            this.appendSeparator = builder ->
-                builder.components.isEmpty() ? builder : builder.append(separator);
-            this.prependPrefix = builder -> builder.prepend(prefix);
-            this.appendSuffix = builder -> builder.append(suffix);
-        }
-
-        /**
-         * Join strings using {@code separator} and delimit the result with
-         * {@code prefix} and {@code suffix}.
-         */
-        public Joiner(String separator, String prefix, String suffix) {
-            this(
-                MalleableString.anyOf(separator),
-                MalleableString.anyOf(prefix),
-                MalleableString.anyOf(suffix)
-            );
-        }
-
-        @Override
-        public Supplier<Builder> supplier() {
-            return Builder::new;
-        }
-
-        @Override
-        public BiConsumer<Builder, MalleableString> accumulator() {
-            return (b, ms) -> appendSeparator.apply(b).append(ms);
-        }
-
-        @Override
-        public BinaryOperator<Builder> combiner() {
-            return (builder0, builder1) -> {
-                builder1.components.forEach(ms -> accumulator().accept(builder0, ms));
-                return builder0;
-            };
-        }
-
-        @Override
-        public Function<Builder, MalleableString> finisher() {
-            return ((Function<Builder, MalleableString>) Builder::get)
-                .compose(appendSuffix).compose(prependPrefix);
-        }
-
-        @Override
-        public Set<Characteristics> characteristics() {
-            return Set.of();
-        }
+    private int inlineCommentStartColumn(
+        List<String> stringComponents, List<List<String>> comments) {
+      final int[] lineLengths =
+          getLinesOfInterest(stringComponents, comments).stream()
+              .mapToInt(String::length)
+              .toArray();
+      int minNonCommentWidth = Integer.MAX_VALUE;
+      int maxNonIgnoredCommentWidth = 0;
+      int numIgnored = lineLengths.length;
+      for (int i : lineLengths) {
+        if (i > 0) minNonCommentWidth = Math.min(minNonCommentWidth, i);
+      }
+      for (int i : lineLengths) {
+        if (i < minNonCommentWidth + FormattingUtils.MAX_WHITESPACE_USED_FOR_ALIGNMENT) {
+          maxNonIgnoredCommentWidth = Math.max(maxNonIgnoredCommentWidth, i);
+          numIgnored--;
+        }
+      }
+      if (numIgnored > lineLengths.length / 2) return 0;
+      final int padding = 2;
+      final int maxCommentWidth =
+          comments.stream()
+              .mapToInt(list -> list.stream().mapToInt(String::length).sum())
+              .max()
+              .orElse(0);
+      return maxNonIgnoredCommentWidth + padding + maxCommentWidth <= width
+          ? maxNonIgnoredCommentWidth + padding
+          : 0;
     }
 
-    /** The result of rendering a {@code MalleableString}. */
-    public record RenderResult(
-        Stream<String> unplacedComments,
-        String rendering,
-        int levelsOfCommentDisplacement
-    ) {
-        private RenderResult with(Stream<String> moreUnplacedComments) {
-            return new RenderResult(
-                Stream.concat(moreUnplacedComments, unplacedComments),
-                rendering,
-                levelsOfCommentDisplacement
-            );
-        }
+    private List<String> getLinesOfInterest(
+        List<String> stringComponents, List<List<String>> comments) {
+      List<Integer> lineNumbersOfInterest = new ArrayList<>();
+      int line = 0;
+      for (int i = 0; i < stringComponents.size(); i++) {
+        if (!comments.get(i).isEmpty()) lineNumbersOfInterest.add(line);
+        int idx = 0;
+        while ((idx = stringComponents.get(i).indexOf("\n", idx) + 1) > 0) line++;
+      }
+      final List<String> lines = String.join("", stringComponents).lines().toList();
+      return lineNumbersOfInterest.stream().map(lines::get).toList();
     }
 
-    /** Represent a list of items that should be rendered in sequence. */
-    private static final class Sequence extends MalleableString {
-
-        private final ImmutableList<MalleableString> components;
-        private Sequence(ImmutableList<MalleableString> components) {
-            this.components = components;
-        }
-
-        private boolean keepCommentsOnSameLine = false;
-        private int width = 0;
-
-        @Override
-        public RenderResult render(int indentation, String singleLineCommentPrefix) {
-            List<RenderResult> componentRenderings = components.stream()
-                .map(malleableString ->
-                         malleableString.render(indentation, singleLineCommentPrefix)
-                ).toList();
-            List<List<String>> commentsFromChildren = componentRenderings.stream()
-                .map(it -> it.unplacedComments).map(Stream::toList).toList();
-            List<String> stringComponents =  componentRenderings.stream()
-                .map(it -> it.rendering)
-                .map(StringUtil::normalizeEol)
-                .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
-            List<String> commentsThatCouldNotBeHandledHere = new ArrayList<>();
-            int startColumn = inlineCommentStartColumn(stringComponents, commentsFromChildren);
-            int numCommentsDisplacedHere = 0;
-            if (
-                commentsFromChildren.stream().anyMatch(s -> !s.isEmpty())
-            ) {
-                for (int i = 0; i < commentsFromChildren.size(); i++) {
-                    if (!FormattingUtils.placeComment(
-                        commentsFromChildren.get(i),
-                        stringComponents,
-                        i,
-                        width,
-                        keepCommentsOnSameLine,
-                        singleLineCommentPrefix,
-                        startColumn
-                    )) {
-                        commentsThatCouldNotBeHandledHere.addAll(commentsFromChildren.get(i));
-                        if (i != 0) numCommentsDisplacedHere++;
-                    }
-                }
-            }
-            return new RenderResult(
-                Stream.concat(this.comments.stream(), commentsThatCouldNotBeHandledHere.stream()),
-                String.join("", stringComponents),
-                componentRenderings.stream()
-                    .mapToInt(RenderResult::levelsOfCommentDisplacement).sum()
-                    + numCommentsDisplacedHere
-            );
-        }
-
-        /**
-         * Return the ideal starting column of an aligned comment, or 0 if
-         * comments should not be aligned.
-         * @param stringComponents The non-comment components of the sequence.
-         * @param comments The comments that will need to be incorporated into
-         * the sequence's representation.
-         */
-        private int inlineCommentStartColumn(
-            List<String> stringComponents,
-            List<List<String>> comments
-        ) {
-            final int[] lineLengths = getLinesOfInterest(stringComponents, comments).stream()
-                .mapToInt(String::length)
-                .toArray();
-            int minNonCommentWidth = Integer.MAX_VALUE;
-            int maxNonIgnoredCommentWidth = 0;
-            int numIgnored = lineLengths.length;
-            for (int i : lineLengths) {
-                if (i > 0) minNonCommentWidth = Math.min(minNonCommentWidth, i);
-            }
-            for (int i : lineLengths) {
-                if (i < minNonCommentWidth + FormattingUtils.MAX_WHITESPACE_USED_FOR_ALIGNMENT) {
-                    maxNonIgnoredCommentWidth = Math.max(maxNonIgnoredCommentWidth, i);
-                    numIgnored--;
-                }
-            }
-            if (numIgnored > lineLengths.length / 2) return 0;
-            final int padding = 2;
-            final int maxCommentWidth = comments.stream()
-                .mapToInt(list -> list.stream().mapToInt(String::length).sum())
-                .max().orElse(0);
-            return maxNonIgnoredCommentWidth + padding + maxCommentWidth <= width ?
-                maxNonIgnoredCommentWidth + padding : 0;
-        }
-
-        private List<String> getLinesOfInterest(
-            List<String> stringComponents,
-            List<List<String>> comments
-        ) {
-            List<Integer> lineNumbersOfInterest = new ArrayList<>();
-            int line = 0;
-            for (int i = 0; i < stringComponents.size(); i++) {
-                if (!comments.get(i).isEmpty()) lineNumbersOfInterest.add(line);
-                int idx = 0;
-                while ((idx = stringComponents.get(i).indexOf("\n", idx) + 1) > 0) line++;
-            }
-            final List<String> lines = String.join("", stringComponents).lines().toList();
-            return lineNumbersOfInterest.stream().map(lines::get).toList();
-        }
-
-        @Override
-        public void findBestRepresentation(
-            Supplier<RenderResult> providedRender,
-            ToLongFunction<RenderResult> badness,
-            int width,
-            int indentation,
-            String singleLineCommentPrefix
-        ) {
-            this.width = width;
-            keepCommentsOnSameLine = true;
-            optimizeChildren(providedRender, badness, width, indentation, singleLineCommentPrefix);
-            if (
-                components.stream().noneMatch(
-                    it -> it.render(indentation, singleLineCommentPrefix)
-                        .unplacedComments.findAny().isPresent()
-                )
-            ) return;
-            long badnessTrue = badness.applyAsLong(providedRender.get());
-            keepCommentsOnSameLine = false;
-            optimizeChildren(providedRender, badness, width, indentation, singleLineCommentPrefix);
-            long badnessFalse = badness.applyAsLong(providedRender.get());
-            keepCommentsOnSameLine = badnessTrue < badnessFalse;
-            optimizeChildren(providedRender, badness, width, indentation, singleLineCommentPrefix);
-            optimizeChildren(providedRender, badness, width, indentation, singleLineCommentPrefix);
-        }
+    @Override
+    public void findBestRepresentation(
+        Supplier<RenderResult> providedRender,
+        ToLongFunction<RenderResult> badness,
+        int width,
+        int indentation,
+        String singleLineCommentPrefix) {
+      this.width = width;
+      keepCommentsOnSameLine = true;
+      optimizeChildren(providedRender, badness, width, indentation, singleLineCommentPrefix);
+      if (components.stream()
+          .noneMatch(
+              it ->
+                  it.render(indentation, singleLineCommentPrefix)
+                      .unplacedComments
+                      .findAny()
+                      .isPresent())) return;
+      long badnessTrue = badness.applyAsLong(providedRender.get());
+      keepCommentsOnSameLine = false;
+      optimizeChildren(providedRender, badness, width, indentation, singleLineCommentPrefix);
+      long badnessFalse = badness.applyAsLong(providedRender.get());
+      keepCommentsOnSameLine = badnessTrue < badnessFalse;
+      optimizeChildren(providedRender, badness, width, indentation, singleLineCommentPrefix);
+      optimizeChildren(providedRender, badness, width, indentation, singleLineCommentPrefix);
+    }
 
-        private void optimizeChildren(
-            Supplier<RenderResult> providedRender,
-            ToLongFunction<RenderResult> badness,
-            int width,
-            int indentation,
-            String singleLineCommentPrefix
-        ) {
-            components.reverse().forEach(it -> it.findBestRepresentation(
-                providedRender, badness, width, indentation, singleLineCommentPrefix
-            ));
-        }
+    private void optimizeChildren(
+        Supplier<RenderResult> providedRender,
+        ToLongFunction<RenderResult> badness,
+        int width,
+        int indentation,
+        String singleLineCommentPrefix) {
+      components
+          .reverse()
+          .forEach(
+              it ->
+                  it.findBestRepresentation(
+                      providedRender, badness, width, indentation, singleLineCommentPrefix));
+    }
 
-        @Override
-        public boolean isEmpty() {
-            return components.stream().allMatch(MalleableString::isEmpty);
-        }
+    @Override
+    public boolean isEmpty() {
+      return components.stream().allMatch(MalleableString::isEmpty);
     }
+  }
 
-    /** Represent an indented version of another {@code MalleableString}. */
-    private static final class Indented extends MalleableString {
+  /** Represent an indented version of another {@code MalleableString}. */
+  private static final class Indented extends MalleableString {
 
-        private final MalleableString nested;
-        private int width;
+    private final MalleableString nested;
+    private int width;
 
-        private Indented(MalleableString toIndent) {
-            this.nested = toIndent;
-        }
+    private Indented(MalleableString toIndent) {
+      this.nested = toIndent;
+    }
 
-        @Override
-        public MalleableString indent() {
-            return new Indented(this);
-        }
+    @Override
+    public MalleableString indent() {
+      return new Indented(this);
+    }
 
-        @Override
-        public void findBestRepresentation(
-            Supplier<RenderResult> providedRender,
-            ToLongFunction<RenderResult> badness,
-            int width,
-            int indentation,
-            String singleLineCommentPrefix
-        ) {
-            this.width = width;
-            nested.findBestRepresentation(
-                providedRender,
-                badness,
-                width - indentation,
-                indentation,
-                singleLineCommentPrefix
-            );
-        }
+    @Override
+    public void findBestRepresentation(
+        Supplier<RenderResult> providedRender,
+        ToLongFunction<RenderResult> badness,
+        int width,
+        int indentation,
+        String singleLineCommentPrefix) {
+      this.width = width;
+      nested.findBestRepresentation(
+          providedRender, badness, width - indentation, indentation, singleLineCommentPrefix);
+    }
 
-        @Override
-        public boolean isEmpty() {
-            return nested.isEmpty();
-        }
+    @Override
+    public boolean isEmpty() {
+      return nested.isEmpty();
+    }
 
-        @Override
-        public RenderResult render(int indentation, String singleLineCommentPrefix) {
-            var result = nested.render(indentation, singleLineCommentPrefix);
-            String renderedComments = FormattingUtils.lineWrapComments(
-                result.unplacedComments.toList(),
-                width - indentation,
-                singleLineCommentPrefix
-            );
-            return new RenderResult(
-                this.comments.stream(),
-                (
-                    renderedComments.isBlank() ? result.rendering
-                        : renderedComments + "\n" + result.rendering
-                ).replaceAll(
-                    "(?<=\n|^)(?=\\h*\\S)",
-                    " ".repeat(indentation)
-                ),
-                result.levelsOfCommentDisplacement()
-            );
-        }
+    @Override
+    public RenderResult render(int indentation, String singleLineCommentPrefix) {
+      var result = nested.render(indentation, singleLineCommentPrefix);
+      String renderedComments =
+          FormattingUtils.lineWrapComments(
+              result.unplacedComments.toList(), width - indentation, singleLineCommentPrefix);
+      return new RenderResult(
+          this.comments.stream(),
+          (renderedComments.isBlank()
+                  ? result.rendering
+                  : renderedComments + "\n" + result.rendering)
+              .replaceAll("(?<=\n|^)(?=\\h*\\S)", " ".repeat(indentation)),
+          result.levelsOfCommentDisplacement());
     }
+  }
 
-    /**
-     * Represent a {@code MalleableString} that admits multiple possible
-     * representations.
-     */
-    private abstract static class MalleableStringWithAlternatives<T> extends MalleableString {
-        protected abstract List<T> getPossibilities();
+  /** Represent a {@code MalleableString} that admits multiple possible representations. */
+  private abstract static class MalleableStringWithAlternatives<T> extends MalleableString {
+    protected abstract List<T> getPossibilities();
 
-        private T bestPossibility;
+    private T bestPossibility;
 
-        @Override
-        public String toString() {
-            return getChosenPossibility().toString();
-        }
+    @Override
+    public String toString() {
+      return getChosenPossibility().toString();
+    }
 
-        @Override
-        public void findBestRepresentation(
-            Supplier<RenderResult> providedRender,
-            ToLongFunction<RenderResult> badness,
-            int width,
-            int indentation, String singleLineCommentPrefix) {
-            bestPossibility = Collections.min(getPossibilities(), (a, b) -> {
+    @Override
+    public void findBestRepresentation(
+        Supplier<RenderResult> providedRender,
+        ToLongFunction<RenderResult> badness,
+        int width,
+        int indentation,
+        String singleLineCommentPrefix) {
+      bestPossibility =
+          Collections.min(
+              getPossibilities(),
+              (a, b) -> {
                 bestPossibility = a;
                 long badnessA = badness.applyAsLong(providedRender.get());
                 bestPossibility = b;
                 long badnessB = badness.applyAsLong(providedRender.get());
                 return Math.toIntExact(badnessA - badnessB);
-            });
-            if (bestPossibility instanceof MalleableString ms) {
-                ms.findBestRepresentation(
-                    providedRender,
-                    badness,
-                    width,
-                    indentation,
-                    singleLineCommentPrefix
-                );
-            }
-        }
+              });
+      if (bestPossibility instanceof MalleableString ms) {
+        ms.findBestRepresentation(
+            providedRender, badness, width, indentation, singleLineCommentPrefix);
+      }
+    }
 
-        /** Return the best representation of this. */
-        protected T getChosenPossibility() {
-            if (getPossibilities().isEmpty()) {
-                throw new IllegalStateException(
-                    "A MalleableString must be directly or transitively backed "
-                        + "by at least one String."
-                );
-            }
-            return bestPossibility == null ? getPossibilities().get(0) : bestPossibility;
-        }
+    /** Return the best representation of this. */
+    protected T getChosenPossibility() {
+      if (getPossibilities().isEmpty()) {
+        throw new IllegalStateException(
+            "A MalleableString must be directly or transitively backed "
+                + "by at least one String.");
+      }
+      return bestPossibility == null ? getPossibilities().get(0) : bestPossibility;
     }
+  }
 
-    /**
-     * A {@code Fork} can be represented by multiple possible
-     * {@code MalleableString}s.
-     */
-    private static final class Fork extends MalleableStringWithAlternatives<MalleableString> {
-        private final ImmutableList<MalleableString> possibilities;
-        private Fork(MalleableString[] possibilities) {
-            this.possibilities = ImmutableList.copyOf(possibilities);
-        }
+  /** A {@code Fork} can be represented by multiple possible {@code MalleableString}s. */
+  private static final class Fork extends MalleableStringWithAlternatives<MalleableString> {
+    private final ImmutableList<MalleableString> possibilities;
 
-        @Override
-        protected List<MalleableString> getPossibilities() { return this.possibilities; }
+    private Fork(MalleableString[] possibilities) {
+      this.possibilities = ImmutableList.copyOf(possibilities);
+    }
 
-        @Override
-        public boolean isEmpty() {
-            return possibilities.stream().allMatch(MalleableString::isEmpty);
-        }
+    @Override
+    protected List<MalleableString> getPossibilities() {
+      return this.possibilities;
+    }
 
-        @Override
-        public RenderResult render(int indentation, String singleLineCommentPrefix) {
-            return getChosenPossibility().render(indentation, singleLineCommentPrefix)
-                .with(comments.stream());
-        }
+    @Override
+    public boolean isEmpty() {
+      return possibilities.stream().allMatch(MalleableString::isEmpty);
     }
 
-    /**
-     * A {@code Leaf} can be represented by multiple possible {@code String}s.
-     */
-    private static final class Leaf extends MalleableStringWithAlternatives<String> {
-        private final ImmutableList<String> possibilities;
-        private Leaf(String[] possibilities) {
-            this.possibilities = ImmutableList.copyOf(possibilities);
-        }
-        private Leaf(String possibility) {
-            this.possibilities = ImmutableList.of(possibility);
-        }
+    @Override
+    public RenderResult render(int indentation, String singleLineCommentPrefix) {
+      return getChosenPossibility()
+          .render(indentation, singleLineCommentPrefix)
+          .with(comments.stream());
+    }
+  }
 
-        @Override
-        protected List<String> getPossibilities() { return this.possibilities; }
+  /** A {@code Leaf} can be represented by multiple possible {@code String}s. */
+  private static final class Leaf extends MalleableStringWithAlternatives<String> {
+    private final ImmutableList<String> possibilities;
 
-        @Override
-        public boolean isEmpty() {
-            return possibilities.stream().allMatch(String::isEmpty);
-        }
+    private Leaf(String[] possibilities) {
+      this.possibilities = ImmutableList.copyOf(possibilities);
+    }
 
-        @Override
-        public RenderResult render(int indentation, String singleLineCommentPrefix) {
-            return new RenderResult(comments.stream(), getChosenPossibility(), 0);
-        }
+    private Leaf(String possibility) {
+      this.possibilities = ImmutableList.of(possibility);
+    }
+
+    @Override
+    protected List<String> getPossibilities() {
+      return this.possibilities;
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return possibilities.stream().allMatch(String::isEmpty);
+    }
+
+    @Override
+    public RenderResult render(int indentation, String singleLineCommentPrefix) {
+      return new RenderResult(comments.stream(), getChosenPossibility(), 0);
     }
+  }
 }
diff --git a/org.lflang/src/org/lflang/ast/ToLf.java b/org.lflang/src/org/lflang/ast/ToLf.java
index 129ec3c35..ad2ccc6a2 100644
--- a/org.lflang/src/org/lflang/ast/ToLf.java
+++ b/org.lflang/src/org/lflang/ast/ToLf.java
@@ -14,14 +14,12 @@ import java.util.function.Supplier;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-
 import org.eclipse.emf.common.util.EList;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.xtext.nodemodel.ICompositeNode;
 import org.eclipse.xtext.nodemodel.INode;
 import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
 import org.eclipse.xtext.xbase.lib.StringExtensions;
-
 import org.lflang.ASTUtils;
 import org.lflang.ast.MalleableString.Builder;
 import org.lflang.ast.MalleableString.Joiner;
@@ -79,345 +77,316 @@ import org.lflang.lf.util.LfSwitch;
 import org.lflang.util.StringUtil;
 
 /**
- * Switch class for converting AST nodes to their textual representation as
- * it would appear in LF code.
+ * Switch class for converting AST nodes to their textual representation as it would appear in LF
+ * code.
  */
 public class ToLf extends LfSwitch<MalleableString> {
 
-    private static final Pattern KEEP_FORMAT_COMMENT
-        = Pattern.compile("\\s*(//|#)\\s*keep-format\\s*");
-
-    /// public instance initialized when loading the class
-    public static final ToLf instance = new ToLf();
-
-    // private constructor
-    private ToLf() { super(); }
-
-    @Override
-    public MalleableString caseArraySpec(ArraySpec spec) {
-        if (spec.isOfVariableLength()) return MalleableString.anyOf("[]");
-        return list("", "[", "]", false, false, spec.getLength());
-    }
-
-    @Override
-    public MalleableString doSwitch(EObject eObject) {
-        ICompositeNode node = NodeModelUtils.findActualNodeFor(eObject);
-        var ancestorComments = getAncestorComments(node);
-        Predicate<INode> doesNotBelongToAncestor = n -> !ancestorComments.contains(n);
-        List<String> followingComments = getFollowingComments(
-            node,
-            ASTUtils.sameLine(node).and(doesNotBelongToAncestor)
-        ).toList();
-        var previous = getNextCompositeSibling(node, INode::getPreviousSibling);
-        Predicate<INode> doesNotBelongToPrevious = doesNotBelongToAncestor.and(
-            previous == null ? n -> true : ASTUtils.sameLine(previous).negate()
-        );
-        Stream<String> precedingComments = ASTUtils.getPrecedingComments(
-            node,
-            doesNotBelongToPrevious
-        ).map(String::strip);
-        Collection<String> allComments = new ArrayList<>();
-        precedingComments.forEachOrdered(allComments::add);
-        getContainedComments(node).stream()
-            .filter(doesNotBelongToAncestor)
-            .map(INode::getText)
-            .forEachOrdered(allComments::add);
-        allComments.addAll(followingComments);
-        if (allComments.stream().anyMatch(s -> KEEP_FORMAT_COMMENT.matcher(s).matches())) {
-            return MalleableString.anyOf(StringUtil.trimCodeBlock(node.getText(), 0))
-                .addComments(followingComments.stream());
-        }
-        return super.doSwitch(eObject).addComments(allComments.stream());
-    }
-
-    /**
-     * Return all comments contained by ancestors of {@code node} that belong to
-     * said ancestors.
-     */
-    static Set<INode> getAncestorComments(INode node) {
-        Set<INode> ancestorComments = new HashSet<>();
-        for (
-            ICompositeNode ancestor = node.getParent();
-            ancestor != null;
-            ancestor = ancestor.getParent()
-        ) {
-            ancestorComments.addAll(getContainedComments(ancestor));
-            ASTUtils.getPrecedingCommentNodes(ancestor, u -> true)
-                .forEachOrdered(ancestorComments::add);
-        }
-        return ancestorComments;
-    }
-
-    /**
-     * Return the next composite sibling of {@code node}, as given by sequential
-     * application of {@code getNextSibling}.
-     */
-    static ICompositeNode getNextCompositeSibling(
-        INode node,
-        Function<INode, INode> getNextSibling
-    ) {
-        INode sibling = node;
-        while ((sibling = getNextSibling.apply(sibling)) != null) {
-            if (
-                sibling instanceof ICompositeNode compositeSibling
-                    && !sibling.getText().isBlank()
-            ) return compositeSibling;
-        }
-        return null;
-    }
-
-    /**
-     * Return the siblings following {@code node} up to (and not including) the
-     * next non-leaf sibling.
-     */
-    private static Stream<INode> getFollowingNonCompositeSiblings(ICompositeNode node) {
-        INode sibling = node;
-        List<INode> ret = new ArrayList<>();
-        while (
-            (sibling = sibling.getNextSibling()) != null
-                && !(sibling instanceof ICompositeNode)
-        ) {
-            ret.add(sibling);
-        }
-        return ret.stream();
-    }
-
-    /**
-     * Return comments that follow {@code node} in the source code and that
-     * either satisfy {@code filter} or that cannot belong to any following
-     * sibling of {@code node}.
-     */
-    private static Stream<String> getFollowingComments(
-        ICompositeNode node,
-        Predicate<INode> filter
-    ) {
-        ICompositeNode sibling = getNextCompositeSibling(node, INode::getNextSibling);
-        Stream<String> followingSiblingComments = getFollowingNonCompositeSiblings(node)
-            .filter(ASTUtils::isComment).map(INode::getText);
-        if (sibling == null) return followingSiblingComments;
-        return Stream.concat(
-            followingSiblingComments,
-            ASTUtils.getPrecedingComments(sibling, filter)
-        );
-    }
-
-    /**
-     * Return comments contained by {@code node} that logically belong to this
-     * node (and not to any of its children).
-     */
-    private static List<INode> getContainedComments(INode node) {
-        ArrayList<INode> ret = new ArrayList<>();
-        boolean inSemanticallyInsignificantLeadingRubbish = true;
-        for (INode child : node.getAsTreeIterable()) {
-            if (!inSemanticallyInsignificantLeadingRubbish && ASTUtils.isComment(child)) {
-                ret.add(child);
-            } else if (!(child instanceof ICompositeNode) && !child.getText().isBlank()) {
-                inSemanticallyInsignificantLeadingRubbish = false;
-            }
-            if (!(child instanceof ICompositeNode)
-                    && (child.getText().contains("\n") || child.getText().contains("\r"))
-                    && !inSemanticallyInsignificantLeadingRubbish
-            ) {
-                break;
-            }
-        }
-        return ret;
-    }
-
-    @Override
-    public MalleableString caseCode(Code code) {
-        String content = ToText.instance.doSwitch(code).lines()
+  private static final Pattern KEEP_FORMAT_COMMENT =
+      Pattern.compile("\\s*(//|#)\\s*keep-format\\s*");
+
+  /// public instance initialized when loading the class
+  public static final ToLf instance = new ToLf();
+
+  // private constructor
+  private ToLf() {
+    super();
+  }
+
+  @Override
+  public MalleableString caseArraySpec(ArraySpec spec) {
+    if (spec.isOfVariableLength()) return MalleableString.anyOf("[]");
+    return list("", "[", "]", false, false, spec.getLength());
+  }
+
+  @Override
+  public MalleableString doSwitch(EObject eObject) {
+    ICompositeNode node = NodeModelUtils.findActualNodeFor(eObject);
+    var ancestorComments = getAncestorComments(node);
+    Predicate<INode> doesNotBelongToAncestor = n -> !ancestorComments.contains(n);
+    List<String> followingComments =
+        getFollowingComments(node, ASTUtils.sameLine(node).and(doesNotBelongToAncestor)).toList();
+    var previous = getNextCompositeSibling(node, INode::getPreviousSibling);
+    Predicate<INode> doesNotBelongToPrevious =
+        doesNotBelongToAncestor.and(
+            previous == null ? n -> true : ASTUtils.sameLine(previous).negate());
+    Stream<String> precedingComments =
+        ASTUtils.getPrecedingComments(node, doesNotBelongToPrevious).map(String::strip);
+    Collection<String> allComments = new ArrayList<>();
+    precedingComments.forEachOrdered(allComments::add);
+    getContainedComments(node).stream()
+        .filter(doesNotBelongToAncestor)
+        .map(INode::getText)
+        .forEachOrdered(allComments::add);
+    allComments.addAll(followingComments);
+    if (allComments.stream().anyMatch(s -> KEEP_FORMAT_COMMENT.matcher(s).matches())) {
+      return MalleableString.anyOf(StringUtil.trimCodeBlock(node.getText(), 0))
+          .addComments(followingComments.stream());
+    }
+    return super.doSwitch(eObject).addComments(allComments.stream());
+  }
+
+  /** Return all comments contained by ancestors of {@code node} that belong to said ancestors. */
+  static Set<INode> getAncestorComments(INode node) {
+    Set<INode> ancestorComments = new HashSet<>();
+    for (ICompositeNode ancestor = node.getParent();
+        ancestor != null;
+        ancestor = ancestor.getParent()) {
+      ancestorComments.addAll(getContainedComments(ancestor));
+      ASTUtils.getPrecedingCommentNodes(ancestor, u -> true).forEachOrdered(ancestorComments::add);
+    }
+    return ancestorComments;
+  }
+
+  /**
+   * Return the next composite sibling of {@code node}, as given by sequential application of {@code
+   * getNextSibling}.
+   */
+  static ICompositeNode getNextCompositeSibling(INode node, Function<INode, INode> getNextSibling) {
+    INode sibling = node;
+    while ((sibling = getNextSibling.apply(sibling)) != null) {
+      if (sibling instanceof ICompositeNode compositeSibling && !sibling.getText().isBlank())
+        return compositeSibling;
+    }
+    return null;
+  }
+
+  /**
+   * Return the siblings following {@code node} up to (and not including) the next non-leaf sibling.
+   */
+  private static Stream<INode> getFollowingNonCompositeSiblings(ICompositeNode node) {
+    INode sibling = node;
+    List<INode> ret = new ArrayList<>();
+    while ((sibling = sibling.getNextSibling()) != null && !(sibling instanceof ICompositeNode)) {
+      ret.add(sibling);
+    }
+    return ret.stream();
+  }
+
+  /**
+   * Return comments that follow {@code node} in the source code and that either satisfy {@code
+   * filter} or that cannot belong to any following sibling of {@code node}.
+   */
+  private static Stream<String> getFollowingComments(ICompositeNode node, Predicate<INode> filter) {
+    ICompositeNode sibling = getNextCompositeSibling(node, INode::getNextSibling);
+    Stream<String> followingSiblingComments =
+        getFollowingNonCompositeSiblings(node).filter(ASTUtils::isComment).map(INode::getText);
+    if (sibling == null) return followingSiblingComments;
+    return Stream.concat(followingSiblingComments, ASTUtils.getPrecedingComments(sibling, filter));
+  }
+
+  /**
+   * Return comments contained by {@code node} that logically belong to this node (and not to any of
+   * its children).
+   */
+  private static List<INode> getContainedComments(INode node) {
+    ArrayList<INode> ret = new ArrayList<>();
+    boolean inSemanticallyInsignificantLeadingRubbish = true;
+    for (INode child : node.getAsTreeIterable()) {
+      if (!inSemanticallyInsignificantLeadingRubbish && ASTUtils.isComment(child)) {
+        ret.add(child);
+      } else if (!(child instanceof ICompositeNode) && !child.getText().isBlank()) {
+        inSemanticallyInsignificantLeadingRubbish = false;
+      }
+      if (!(child instanceof ICompositeNode)
+          && (child.getText().contains("\n") || child.getText().contains("\r"))
+          && !inSemanticallyInsignificantLeadingRubbish) {
+        break;
+      }
+    }
+    return ret;
+  }
+
+  @Override
+  public MalleableString caseCode(Code code) {
+    String content =
+        ToText.instance
+            .doSwitch(code)
+            .lines()
             .map(String::stripTrailing)
             .collect(Collectors.joining("\n"));
-        MalleableString singleLineRepresentation = MalleableString.anyOf(
-            String.format("{= %s =}", content.strip())
-        );
-        MalleableString multilineRepresentation = new Builder()
+    MalleableString singleLineRepresentation =
+        MalleableString.anyOf(String.format("{= %s =}", content.strip()));
+    MalleableString multilineRepresentation =
+        new Builder()
             .append(String.format("{=%n"))
             .append(MalleableString.anyOf(content).indent())
             .append(String.format("%n=}"))
             .get();
-        if (content.lines().count() > 1 || content.contains("#") || content.contains("//")) {
-            return multilineRepresentation;
-        }
-        return MalleableString.anyOf(singleLineRepresentation, multilineRepresentation);
-    }
-
-    @Override
-    public MalleableString caseHost(Host host) {
-        Builder msb = new Builder();
-        if (!StringExtensions.isNullOrEmpty(host.getUser())) {
-            msb.append(host.getUser()).append("@");
-        }
-        if (!StringExtensions.isNullOrEmpty(host.getAddr())) {
-            msb.append(host.getAddr());
-        }
-        if (host.getPort() != 0) {
-            msb.append(":").append(host.getPort());
-        }
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseLiteral(Literal l) {
-        // STRING | CHAR_LIT | SignedFloat | SignedInt | Boolean
-        return MalleableString.anyOf(l.getLiteral());
-    }
-
-    @Override
-    public MalleableString caseParameterReference(ParameterReference p) {
-        // parameter=[Parameter]
-        return MalleableString.anyOf(p.getParameter().getName());
-    }
-
-    @Override
-    public MalleableString caseAttribute(Attribute object) {
-        // '@' attrName=ID ('(' (attrParms+=AttrParm (',' attrParms+=AttrParm)* ','?)? ')')?
-        var builder = new Builder()
-            .append("@")
-            .append(object.getAttrName());
-        if (object.getAttrParms() != null) {
-            builder.append(list(true, object.getAttrParms()));
-        }
-        return builder.get();
-    }
-
-    @Override
-    public MalleableString caseAttrParm(AttrParm object) {
-        // (name=ID '=')? value=AttrParmValue;
-        var builder = new Builder();
-        if (object.getName() != null) builder.append(object.getName()).append(" = ");
-        return builder.append(object.getValue()).get();
-    }
-
-    @Override
-    public MalleableString caseTime(Time t) {
-        // (interval=INT unit=TimeUnit)
-        return MalleableString.anyOf(ASTUtils.toTimeValue(t).toString());
-    }
-
-    @Override
-    public MalleableString caseType(Type type) {
-        // time?='time' (arraySpec=ArraySpec)?
-        // | id=DottedName ('<' typeParms+=Type (',' typeParms+=Type)* '>')? (stars+='*')*
-        //     (arraySpec=ArraySpec)?
-        // | code=Code
-        if (type.getCode() != null) return doSwitch(type.getCode());
-        Builder msb = new Builder();
-        if (type.isTime()) {
-            msb.append("time");
-        } else if (type.getId() != null) {
-            msb.append(type.getId());  // TODO: Multiline dottedName?
-            if (type.getTypeParms() != null) {
-                msb.append(list(", ", "<", ">", true, false, type.getTypeParms()));
-            }
-            msb.append("*".repeat(type.getStars().size()));
-        }
-        if (type.getArraySpec() != null) msb.append(doSwitch(type.getArraySpec()));
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseTypeParm(TypeParm t) {
-        // literal=TypeExpr | code=Code
-        return MalleableString.anyOf(
-            !StringExtensions.isNullOrEmpty(t.getLiteral()) ?
-            MalleableString.anyOf(t.getLiteral()) : doSwitch(t.getCode())
-        );
-    }
-
-    @Override
-    public MalleableString caseVarRef(VarRef v) {
-        // variable=[Variable] | container=[Instantiation] '.' variable=[Variable]
-        // | interleaved?='interleaved' '(' (variable=[Variable] | container=[Instantiation] '.'
-        //     variable=[Variable]) ')'
-        if (!v.isInterleaved()) return MalleableString.anyOf(ToText.instance.doSwitch(v));
-        return new Builder()
-            .append("interleaved ")
-            .append(list(false, ToText.instance.doSwitch(v)))
-            .get();
-    }
-
-    @Override
-    public MalleableString caseModel(Model object) {
-        // target=TargetDecl
-        // (imports+=Import)*
-        // (preambles+=Preamble)*
-        // (reactors+=Reactor)+
-        Builder msb = new Builder();
-        msb.append(doSwitch(object.getTarget())).append("\n".repeat(2));
-        object.getImports().forEach(i -> msb.append(doSwitch(i)).append("\n"));
-        if (!object.getImports().isEmpty()) msb.append("\n");
-        object.getPreambles().forEach(
-            p -> msb.append(doSwitch(p)).append("\n".repeat(2))
-        );
-        msb.append(
-             object.getReactors().stream().map(this::doSwitch)
-                   .collect(new Joiner("\n".repeat(2)))
-        ).append("\n");
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseImport(Import object) {
-        // 'import' reactorClasses+=ImportedReactor (',' reactorClasses+=ImportedReactor)*
-        //     'from' importURI=STRING ';'?
-        // TODO: Break this. Break string, break at whitespace outside the string.
-        return new Builder()
-            .append("import ")
-            // TODO: This is a place where we can use conditional parentheses.
-            .append(list(", ", "", "", false, true, object.getReactorClasses()))
-            .append(" from \"")
-            .append(object.getImportURI())
-            .append("\"")
-            .get();
-    }
-
-    @Override
-    public MalleableString caseReactorDecl(ReactorDecl object) {
-        // Reactor | ImportedReactor
-        return defaultCase(object);
-    }
-
-    @Override
-    public MalleableString caseImportedReactor(ImportedReactor object) {
-        // reactorClass=[Reactor] ('as' name=ID)?
-        if (object.getName() != null) {
-            return MalleableString.anyOf(String.format(
-                "%s as %s",
-                object.getReactorClass().getName(),
-                object.getName()
-            ));
-        }
-        return MalleableString.anyOf(object.getReactorClass().getName());
-    }
-
-    @Override
-    public MalleableString caseReactor(Reactor object) {
-        // {Reactor} ((federated?='federated' | main?='main')? & realtime?='realtime'?)
-        //     'reactor' (name=ID)?
-        // ('<' typeParms+=TypeParm (',' typeParms+=TypeParm)* '>')?
-        // ('(' parameters+=Parameter (',' parameters+=Parameter)* ')')?
-        // ('at' host=Host)?
-        // ('extends' (superClasses+=[ReactorDecl] (',' superClasses+=[ReactorDecl])*))?
-        // '{'
-        // (     (preambles+=Preamble)
-        //     | (stateVars+=StateVar)
-        //     | (methods+=Method)
-        //     | (inputs+=Input)
-        //     | (outputs+=Output)
-        //     | (timers+=Timer)
-        //     | (actions+=Action)
-        //     | (instantiations+=Instantiation)
-        //     | (connections+=Connection)
-        //     | (reactions+=Reaction)
-        //     | (modes+=Mode)
-        //     | (mutations+=Mutation)
-        // )* '}'
-        Builder msb = new Builder();
-        addAttributes(msb, object::getAttributes);
-        msb.append(reactorHeader(object));
-        MalleableString smallFeatures = indentedStatements(
+    if (content.lines().count() > 1 || content.contains("#") || content.contains("//")) {
+      return multilineRepresentation;
+    }
+    return MalleableString.anyOf(singleLineRepresentation, multilineRepresentation);
+  }
+
+  @Override
+  public MalleableString caseHost(Host host) {
+    Builder msb = new Builder();
+    if (!StringExtensions.isNullOrEmpty(host.getUser())) {
+      msb.append(host.getUser()).append("@");
+    }
+    if (!StringExtensions.isNullOrEmpty(host.getAddr())) {
+      msb.append(host.getAddr());
+    }
+    if (host.getPort() != 0) {
+      msb.append(":").append(host.getPort());
+    }
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseLiteral(Literal l) {
+    // STRING | CHAR_LIT | SignedFloat | SignedInt | Boolean
+    return MalleableString.anyOf(l.getLiteral());
+  }
+
+  @Override
+  public MalleableString caseParameterReference(ParameterReference p) {
+    // parameter=[Parameter]
+    return MalleableString.anyOf(p.getParameter().getName());
+  }
+
+  @Override
+  public MalleableString caseAttribute(Attribute object) {
+    // '@' attrName=ID ('(' (attrParms+=AttrParm (',' attrParms+=AttrParm)* ','?)? ')')?
+    var builder = new Builder().append("@").append(object.getAttrName());
+    if (object.getAttrParms() != null) {
+      builder.append(list(true, object.getAttrParms()));
+    }
+    return builder.get();
+  }
+
+  @Override
+  public MalleableString caseAttrParm(AttrParm object) {
+    // (name=ID '=')? value=AttrParmValue;
+    var builder = new Builder();
+    if (object.getName() != null) builder.append(object.getName()).append(" = ");
+    return builder.append(object.getValue()).get();
+  }
+
+  @Override
+  public MalleableString caseTime(Time t) {
+    // (interval=INT unit=TimeUnit)
+    return MalleableString.anyOf(ASTUtils.toTimeValue(t).toString());
+  }
+
+  @Override
+  public MalleableString caseType(Type type) {
+    // time?='time' (arraySpec=ArraySpec)?
+    // | id=DottedName ('<' typeParms+=Type (',' typeParms+=Type)* '>')? (stars+='*')*
+    //     (arraySpec=ArraySpec)?
+    // | code=Code
+    if (type.getCode() != null) return doSwitch(type.getCode());
+    Builder msb = new Builder();
+    if (type.isTime()) {
+      msb.append("time");
+    } else if (type.getId() != null) {
+      msb.append(type.getId()); // TODO: Multiline dottedName?
+      if (type.getTypeParms() != null) {
+        msb.append(list(", ", "<", ">", true, false, type.getTypeParms()));
+      }
+      msb.append("*".repeat(type.getStars().size()));
+    }
+    if (type.getArraySpec() != null) msb.append(doSwitch(type.getArraySpec()));
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseTypeParm(TypeParm t) {
+    // literal=TypeExpr | code=Code
+    return MalleableString.anyOf(
+        !StringExtensions.isNullOrEmpty(t.getLiteral())
+            ? MalleableString.anyOf(t.getLiteral())
+            : doSwitch(t.getCode()));
+  }
+
+  @Override
+  public MalleableString caseVarRef(VarRef v) {
+    // variable=[Variable] | container=[Instantiation] '.' variable=[Variable]
+    // | interleaved?='interleaved' '(' (variable=[Variable] | container=[Instantiation] '.'
+    //     variable=[Variable]) ')'
+    if (!v.isInterleaved()) return MalleableString.anyOf(ToText.instance.doSwitch(v));
+    return new Builder()
+        .append("interleaved ")
+        .append(list(false, ToText.instance.doSwitch(v)))
+        .get();
+  }
+
+  @Override
+  public MalleableString caseModel(Model object) {
+    // target=TargetDecl
+    // (imports+=Import)*
+    // (preambles+=Preamble)*
+    // (reactors+=Reactor)+
+    Builder msb = new Builder();
+    msb.append(doSwitch(object.getTarget())).append("\n".repeat(2));
+    object.getImports().forEach(i -> msb.append(doSwitch(i)).append("\n"));
+    if (!object.getImports().isEmpty()) msb.append("\n");
+    object.getPreambles().forEach(p -> msb.append(doSwitch(p)).append("\n".repeat(2)));
+    msb.append(
+            object.getReactors().stream().map(this::doSwitch).collect(new Joiner("\n".repeat(2))))
+        .append("\n");
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseImport(Import object) {
+    // 'import' reactorClasses+=ImportedReactor (',' reactorClasses+=ImportedReactor)*
+    //     'from' importURI=STRING ';'?
+    // TODO: Break this. Break string, break at whitespace outside the string.
+    return new Builder()
+        .append("import ")
+        // TODO: This is a place where we can use conditional parentheses.
+        .append(list(", ", "", "", false, true, object.getReactorClasses()))
+        .append(" from \"")
+        .append(object.getImportURI())
+        .append("\"")
+        .get();
+  }
+
+  @Override
+  public MalleableString caseReactorDecl(ReactorDecl object) {
+    // Reactor | ImportedReactor
+    return defaultCase(object);
+  }
+
+  @Override
+  public MalleableString caseImportedReactor(ImportedReactor object) {
+    // reactorClass=[Reactor] ('as' name=ID)?
+    if (object.getName() != null) {
+      return MalleableString.anyOf(
+          String.format("%s as %s", object.getReactorClass().getName(), object.getName()));
+    }
+    return MalleableString.anyOf(object.getReactorClass().getName());
+  }
+
+  @Override
+  public MalleableString caseReactor(Reactor object) {
+    // {Reactor} ((federated?='federated' | main?='main')? & realtime?='realtime'?)
+    //     'reactor' (name=ID)?
+    // ('<' typeParms+=TypeParm (',' typeParms+=TypeParm)* '>')?
+    // ('(' parameters+=Parameter (',' parameters+=Parameter)* ')')?
+    // ('at' host=Host)?
+    // ('extends' (superClasses+=[ReactorDecl] (',' superClasses+=[ReactorDecl])*))?
+    // '{'
+    // (     (preambles+=Preamble)
+    //     | (stateVars+=StateVar)
+    //     | (methods+=Method)
+    //     | (inputs+=Input)
+    //     | (outputs+=Output)
+    //     | (timers+=Timer)
+    //     | (actions+=Action)
+    //     | (instantiations+=Instantiation)
+    //     | (connections+=Connection)
+    //     | (reactions+=Reaction)
+    //     | (modes+=Mode)
+    //     | (mutations+=Mutation)
+    // )* '}'
+    Builder msb = new Builder();
+    addAttributes(msb, object::getAttributes);
+    msb.append(reactorHeader(object));
+    MalleableString smallFeatures =
+        indentedStatements(
             List.of(
                 object.getPreambles(),
                 object.getInputs(),
@@ -426,662 +395,631 @@ public class ToLf extends LfSwitch<MalleableString> {
                 object.getActions(),
                 object.getInstantiations(),
                 object.getConnections(),
-                object.getStateVars()
-            ),
-            false
-        );
-        MalleableString bigFeatures = indentedStatements(
-            List.of(
-                object.getReactions(),
-                object.getMethods(),
-                object.getModes()
-            ),
-            true
-        );
-        msb.append(smallFeatures);
-        if (!smallFeatures.isEmpty() && !bigFeatures.isEmpty()) {
-            msb.append("\n".repeat(1));
-        }
-        msb.append(bigFeatures);
-        msb.append("}");
-        return msb.get();
-    }
-
-    /** Return the signature of the given reactor. */
-    private MalleableString reactorHeader(Reactor object) {
-        Builder msb = new Builder();
-        if (object.isFederated()) msb.append("federated ");
-        if (object.isMain()) msb.append("main ");
-        if (object.isRealtime()) msb.append("realtime ");
-        msb.append("reactor");
-        if (object.getName() != null) msb.append(" ").append(object.getName());
-        msb.append(list(", ", "<", ">", true, false, object.getTypeParms()));
-        msb.append(list(true, object.getParameters()));
-        if (object.getHost() != null) msb.append(" at ").append(doSwitch(object.getHost()));
-        if (object.getSuperClasses() != null && !object.getSuperClasses().isEmpty()) {
-            msb.append(
-                MalleableString.anyOf(" extends "),
-                new Builder()
-                    .append("\n")
-                    .append(
-                        MalleableString.anyOf("extends ").indent().indent()
-                    )
-                    .get()
-               )
-               .append(
-                object.getSuperClasses().stream()
-                    .map(ReactorDecl::getName)
-                    .collect(Collectors.joining(", "))
-            );
-        }
-        msb.append(String.format(" {%n"));
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseTargetDecl(TargetDecl object) {
-        // 'target' name=ID (config=KeyValuePairs)? ';'?
-        Builder msb = new Builder();
-        msb.append("target ").append(object.getName());
-        if (object.getConfig() != null && !object.getConfig().getPairs().isEmpty()) {
-            msb.append(" ").append(doSwitch(object.getConfig()));
-        }
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseStateVar(StateVar object) {
-        // 'state' name=ID (
-        //     (':' (type=Type))?
-        //     ((parens+='(' (init+=Expression (','  init+=Expression)*)? parens+=')')
-        //         | (braces+='{' (init+=Expression (','  init+=Expression)*)? braces+='}')
-        //     )?
-        // ) ';'?
-        Builder msb = new Builder();
-        addAttributes(msb, object::getAttributes);
-        if (object.isReset()) msb.append("reset ");
-        msb.append("state ").append(object.getName());
-        msb.append(typeAnnotationFor(object.getType()));
-        if (!object.getParens().isEmpty()) msb.append(list(true, object.getInit()));
-        if (!object.getBraces().isEmpty()) {
-            msb.append(list(", ", "{", "}", true, false, object.getInit()));
-        }
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseMethod(Method object) {
-        // const?='const'? 'method' name=ID
-        // '(' (arguments+=MethodArgument (',' arguments+=MethodArgument)*)? ')'
-        // (':' return=Type)?
-        // code=Code
-        // ';'?
-        Builder msb = new Builder();
-        if (object.isConst()) msb.append("const ");
-        msb.append("method ").append(object.getName());
-        msb.append(list(false, object.getArguments()));
-        msb.append(typeAnnotationFor(object.getReturn()))
-            .append(" ")
-            .append(doSwitch(object.getCode()));
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseMethodArgument(MethodArgument object) {
-        // name=ID (':' type=Type)?
-        return new Builder().append(object.getName()).append(typeAnnotationFor(object.getType())).get();
-    }
-
-    @Override
-    public MalleableString caseInput(Input object) {
-        // mutable?='mutable'? 'input' (widthSpec=WidthSpec)? name=ID (':' type=Type)? ';'?
-        Builder msb = new Builder();
-        addAttributes(msb, object::getAttributes);
-        if (object.isMutable()) msb.append("mutable ");
-        msb.append("input");
-        if (object.getWidthSpec() != null) msb.append(doSwitch(object.getWidthSpec()));
-        msb.append(" ").append(object.getName()).append(typeAnnotationFor(object.getType()));
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseOutput(Output object) {
-        // 'output' (widthSpec=WidthSpec)? name=ID (':' type=Type)? ';'?
-        Builder msb = new Builder();
-        addAttributes(msb, object::getAttributes);
-        msb.append("output");
-        if (object.getWidthSpec() != null) msb.append(doSwitch(object.getWidthSpec()));
-        msb.append(" ").append(object.getName());
-        msb.append(typeAnnotationFor(object.getType()));
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseTimer(Timer object) {
-        // 'timer' name=ID ('(' offset=Expression (',' period=Expression)? ')')? ';'?
-        var builder = new Builder();
-        addAttributes(builder, object::getAttributes);
-        return builder
-            .append("timer ")
-            .append(object.getName())
-            .append(list(true, object.getOffset(), object.getPeriod()))
-            .get();
-    }
-
-    @Override
-    public MalleableString caseMode(Mode object) {
-        // {Mode} (initial?='initial')? 'mode' (name=ID)?
-        // '{' (
-        //     (stateVars+=StateVar) |
-        //     (timers+=Timer) |
-        //     (actions+=Action) |
-        //     (instantiations+=Instantiation) |
-        //     (connections+=Connection) |
-        //     (reactions+=Reaction)
-        // )* '}'
-        Builder msb = new Builder();
-        if (object.isInitial()) msb.append("initial ");
-        msb.append("mode ");
-        if (object.getName() != null) msb.append(object.getName()).append(" ");
-        msb.append(String.format("{%n"));
-        msb.append(indentedStatements(
+                object.getStateVars()),
+            false);
+    MalleableString bigFeatures =
+        indentedStatements(
+            List.of(object.getReactions(), object.getMethods(), object.getModes()), true);
+    msb.append(smallFeatures);
+    if (!smallFeatures.isEmpty() && !bigFeatures.isEmpty()) {
+      msb.append("\n".repeat(1));
+    }
+    msb.append(bigFeatures);
+    msb.append("}");
+    return msb.get();
+  }
+
+  /** Return the signature of the given reactor. */
+  private MalleableString reactorHeader(Reactor object) {
+    Builder msb = new Builder();
+    if (object.isFederated()) msb.append("federated ");
+    if (object.isMain()) msb.append("main ");
+    if (object.isRealtime()) msb.append("realtime ");
+    msb.append("reactor");
+    if (object.getName() != null) msb.append(" ").append(object.getName());
+    msb.append(list(", ", "<", ">", true, false, object.getTypeParms()));
+    msb.append(list(true, object.getParameters()));
+    if (object.getHost() != null) msb.append(" at ").append(doSwitch(object.getHost()));
+    if (object.getSuperClasses() != null && !object.getSuperClasses().isEmpty()) {
+      msb.append(
+              MalleableString.anyOf(" extends "),
+              new Builder()
+                  .append("\n")
+                  .append(MalleableString.anyOf("extends ").indent().indent())
+                  .get())
+          .append(
+              object.getSuperClasses().stream()
+                  .map(ReactorDecl::getName)
+                  .collect(Collectors.joining(", ")));
+    }
+    msb.append(String.format(" {%n"));
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseTargetDecl(TargetDecl object) {
+    // 'target' name=ID (config=KeyValuePairs)? ';'?
+    Builder msb = new Builder();
+    msb.append("target ").append(object.getName());
+    if (object.getConfig() != null && !object.getConfig().getPairs().isEmpty()) {
+      msb.append(" ").append(doSwitch(object.getConfig()));
+    }
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseStateVar(StateVar object) {
+    // 'state' name=ID (
+    //     (':' (type=Type))?
+    //     ((parens+='(' (init+=Expression (','  init+=Expression)*)? parens+=')')
+    //         | (braces+='{' (init+=Expression (','  init+=Expression)*)? braces+='}')
+    //     )?
+    // ) ';'?
+    Builder msb = new Builder();
+    addAttributes(msb, object::getAttributes);
+    if (object.isReset()) msb.append("reset ");
+    msb.append("state ").append(object.getName());
+    msb.append(typeAnnotationFor(object.getType()));
+    if (!object.getParens().isEmpty()) msb.append(list(true, object.getInit()));
+    if (!object.getBraces().isEmpty()) {
+      msb.append(list(", ", "{", "}", true, false, object.getInit()));
+    }
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseMethod(Method object) {
+    // const?='const'? 'method' name=ID
+    // '(' (arguments+=MethodArgument (',' arguments+=MethodArgument)*)? ')'
+    // (':' return=Type)?
+    // code=Code
+    // ';'?
+    Builder msb = new Builder();
+    if (object.isConst()) msb.append("const ");
+    msb.append("method ").append(object.getName());
+    msb.append(list(false, object.getArguments()));
+    msb.append(typeAnnotationFor(object.getReturn()))
+        .append(" ")
+        .append(doSwitch(object.getCode()));
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseMethodArgument(MethodArgument object) {
+    // name=ID (':' type=Type)?
+    return new Builder().append(object.getName()).append(typeAnnotationFor(object.getType())).get();
+  }
+
+  @Override
+  public MalleableString caseInput(Input object) {
+    // mutable?='mutable'? 'input' (widthSpec=WidthSpec)? name=ID (':' type=Type)? ';'?
+    Builder msb = new Builder();
+    addAttributes(msb, object::getAttributes);
+    if (object.isMutable()) msb.append("mutable ");
+    msb.append("input");
+    if (object.getWidthSpec() != null) msb.append(doSwitch(object.getWidthSpec()));
+    msb.append(" ").append(object.getName()).append(typeAnnotationFor(object.getType()));
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseOutput(Output object) {
+    // 'output' (widthSpec=WidthSpec)? name=ID (':' type=Type)? ';'?
+    Builder msb = new Builder();
+    addAttributes(msb, object::getAttributes);
+    msb.append("output");
+    if (object.getWidthSpec() != null) msb.append(doSwitch(object.getWidthSpec()));
+    msb.append(" ").append(object.getName());
+    msb.append(typeAnnotationFor(object.getType()));
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseTimer(Timer object) {
+    // 'timer' name=ID ('(' offset=Expression (',' period=Expression)? ')')? ';'?
+    var builder = new Builder();
+    addAttributes(builder, object::getAttributes);
+    return builder
+        .append("timer ")
+        .append(object.getName())
+        .append(list(true, object.getOffset(), object.getPeriod()))
+        .get();
+  }
+
+  @Override
+  public MalleableString caseMode(Mode object) {
+    // {Mode} (initial?='initial')? 'mode' (name=ID)?
+    // '{' (
+    //     (stateVars+=StateVar) |
+    //     (timers+=Timer) |
+    //     (actions+=Action) |
+    //     (instantiations+=Instantiation) |
+    //     (connections+=Connection) |
+    //     (reactions+=Reaction)
+    // )* '}'
+    Builder msb = new Builder();
+    if (object.isInitial()) msb.append("initial ");
+    msb.append("mode ");
+    if (object.getName() != null) msb.append(object.getName()).append(" ");
+    msb.append(String.format("{%n"));
+    msb.append(
+        indentedStatements(
             List.of(
                 object.getStateVars(),
                 object.getTimers(),
                 object.getActions(),
                 object.getInstantiations(),
-                object.getConnections()
-            ),
-            false
-        ));
-        msb.append(indentedStatements(
-            List.of(object.getReactions()),
-            true
-        ));
-        msb.append("}");
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseAction(Action object) {
-        // (origin=ActionOrigin)? 'action' name=ID
-        // ('(' minDelay=Expression (',' minSpacing=Expression (',' policy=STRING)? )? ')')?
-        // (':' type=Type)? ';'?
-        Builder msb = new Builder();
-        addAttributes(msb, object::getAttributes);
-        if (object.getOrigin() != null) msb.append(object.getOrigin().getLiteral()).append(" ");
-        return msb.append("action ")
-            .append(object.getName())
-            .append(list(
+                object.getConnections()),
+            false));
+    msb.append(indentedStatements(List.of(object.getReactions()), true));
+    msb.append("}");
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseAction(Action object) {
+    // (origin=ActionOrigin)? 'action' name=ID
+    // ('(' minDelay=Expression (',' minSpacing=Expression (',' policy=STRING)? )? ')')?
+    // (':' type=Type)? ';'?
+    Builder msb = new Builder();
+    addAttributes(msb, object::getAttributes);
+    if (object.getOrigin() != null) msb.append(object.getOrigin().getLiteral()).append(" ");
+    return msb.append("action ")
+        .append(object.getName())
+        .append(
+            list(
                 true,
                 object.getMinDelay(),
                 object.getMinSpacing(),
-                object.getPolicy() != null ? String.format("\"%s\"", object.getPolicy()) : null
-            ))
-            .append(typeAnnotationFor(object.getType()))
-            .get();
-    }
-
-    @Override
-    public MalleableString caseReaction(Reaction object) {
-        // ('reaction')
-        // ('(' (triggers+=TriggerRef (',' triggers+=TriggerRef)*)? ')')?
-        // (sources+=VarRef (',' sources+=VarRef)*)?
-        // ('->' effects+=VarRefOrModeTransition (',' effects+=VarRefOrModeTransition)*)?
-        // code=Code
-        // (stp=STP)?
-        // (deadline=Deadline)?
-        Builder msb = new Builder();
-        addAttributes(msb, object::getAttributes);
-        if (object.isMutation()) {
-            msb.append("mutation");
-        } else {
-            msb.append("reaction");
-        }
-        msb.append(list(true, object.getTriggers()));
-        msb.append(list(", ", " ", "", true, false, object.getSources()));
-        if (!object.getEffects().isEmpty()) {
-            List<Mode> allModes = ASTUtils.allModes(ASTUtils.getEnclosingReactor(object));
-            msb.append(" -> ", " ->\n")
-                .append(object.getEffects().stream().map(varRef ->
-                    (allModes.stream().anyMatch(
-                        m -> m.getName().equals(varRef.getVariable().getName())
-                    )) ? new Builder()
-                        .append(varRef.getTransition())
-                        .append("(").append(doSwitch(varRef)).append(")").get()
-                        : doSwitch(varRef)
-                ).collect(new Joiner(", ")));
-        }
-        msb.append(" ").append(doSwitch(object.getCode()));
-        if (object.getStp() != null) msb.append(" ").append(doSwitch(object.getStp()));
-        if (object.getDeadline() != null) msb.append(" ").append(doSwitch(object.getDeadline()));
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseTriggerRef(TriggerRef object) {
-        // BuiltinTriggerRef | VarRef
-        throw new UnsupportedOperationException(
-            "TriggerRefs are BuiltinTriggerRefs or VarRefs, so the methods "
-                + "corresponding to those types should be invoked instead.");
-    }
-
-    @Override
-    public MalleableString caseBuiltinTriggerRef(BuiltinTriggerRef object) {
-        // type = BuiltinTrigger
-        return MalleableString.anyOf(object.getType().getLiteral());
-    }
-
-    @Override
-    public MalleableString caseDeadline(Deadline object) {
-        // 'deadline' '(' delay=Expression ')' code=Code
-        return handler(object, "deadline", Deadline::getDelay, Deadline::getCode);
-    }
-
-    @Override
-    public MalleableString caseSTP(STP object) {
-        // 'STP' '(' value=Expression ')' code=Code
-        return handler(object, "STP", STP::getValue, STP::getCode);
-    }
-
-    private <T extends EObject> MalleableString handler(
-        T object,
-        String name,
-        Function<T, Expression> getTrigger,
-        Function<T, Code> getCode
-    ) {
-        return new Builder()
-            .append(name)
-            .append(list(false, getTrigger.apply(object)))
-            .append(" ")
-            .append(doSwitch(getCode.apply(object)))
-            .get();
-    }
-
-
-    @Override
-    public MalleableString casePreamble(Preamble object) {
-        // (visibility=Visibility)? 'preamble' code=Code
-        Builder msb = new Builder();
-        if (object.getVisibility() != null && object.getVisibility() != Visibility.NONE) {
-            msb.append(object.getVisibility().getLiteral()).append(" ");
-        }
-        return msb.append("preamble ").append(doSwitch(object.getCode())).get();
-    }
-
-    @Override
-    public MalleableString caseInstantiation(Instantiation object) {
-        // name=ID '=' 'new' (widthSpec=WidthSpec)?
-        // reactorClass=[ReactorDecl] ('<' typeParms+=TypeParm (',' typeParms+=TypeParm)* '>')? '('
-        // (parameters+=Assignment (',' parameters+=Assignment)*)?
-        // ')' ('at' host=Host)? ';'?;
-        Builder msb = new Builder();
-        msb.append(object.getName()).append(" = new");
-        if (object.getWidthSpec() != null) msb.append(doSwitch(object.getWidthSpec()));
-        msb.append(" ").append(object.getReactorClass().getName());
-        msb.append(list(", ", "<", ">", true, false, object.getTypeParms()));
-        msb.append(list(false, object.getParameters()));
-        // TODO: Delete the following case when the corresponding feature is removed
-        if (object.getHost() != null) msb.append(" at ").append(doSwitch(object.getHost()));
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseConnection(Connection object) {
-        // ((leftPorts += VarRef (',' leftPorts += VarRef)*)
-        //     | ( '(' leftPorts += VarRef (',' leftPorts += VarRef)* ')' iterated ?= '+'?))
-        // ('->' | physical?='~>')
-        // rightPorts += VarRef (',' rightPorts += VarRef)*
-        //     ('after' delay=Expression)?
-        // (serializer=Serializer)?
-        // ';'?
-        Builder msb = new Builder();
-        if (object.isIterated()) {
-            msb.append(list(false, object.getLeftPorts())).append("+");
-        } else {
-            msb.append(
-                object.getLeftPorts().stream().map(this::doSwitch).collect(new Joiner(", ")),
-                object.getLeftPorts().stream().map(this::doSwitch).collect(
-                    new Joiner(String.format(",%n"))
-                )
-            );
-        }
-        msb.append(
-            "",
-            MalleableString.anyOf("\n").indent()
-        );
-        msb.append(object.isPhysical() ? " ~>" : " ->");
-        msb.append(minimallyDelimitedList(object.getRightPorts()));
-        if (object.getDelay() != null) msb.append(" after ").append(doSwitch(object.getDelay()));
-        if (object.getSerializer() != null) {
-            msb.append(" ").append(doSwitch(object.getSerializer()));
-        }
-        return msb.get();
-    }
-
-    private MalleableString minimallyDelimitedList(List<? extends EObject> items) {
-        return MalleableString.anyOf(
-            list(", ",  " ", "", true, true, items),
+                object.getPolicy() != null ? String.format("\"%s\"", object.getPolicy()) : null))
+        .append(typeAnnotationFor(object.getType()))
+        .get();
+  }
+
+  @Override
+  public MalleableString caseReaction(Reaction object) {
+    // ('reaction')
+    // ('(' (triggers+=TriggerRef (',' triggers+=TriggerRef)*)? ')')?
+    // (sources+=VarRef (',' sources+=VarRef)*)?
+    // ('->' effects+=VarRefOrModeTransition (',' effects+=VarRefOrModeTransition)*)?
+    // code=Code
+    // (stp=STP)?
+    // (deadline=Deadline)?
+    Builder msb = new Builder();
+    addAttributes(msb, object::getAttributes);
+    if (object.isMutation()) {
+      msb.append("mutation");
+    } else {
+      msb.append("reaction");
+    }
+    msb.append(list(true, object.getTriggers()));
+    msb.append(list(", ", " ", "", true, false, object.getSources()));
+    if (!object.getEffects().isEmpty()) {
+      List<Mode> allModes = ASTUtils.allModes(ASTUtils.getEnclosingReactor(object));
+      msb.append(" -> ", " ->\n")
+          .append(
+              object.getEffects().stream()
+                  .map(
+                      varRef ->
+                          (allModes.stream()
+                                  .anyMatch(
+                                      m -> m.getName().equals(varRef.getVariable().getName())))
+                              ? new Builder()
+                                  .append(varRef.getTransition())
+                                  .append("(")
+                                  .append(doSwitch(varRef))
+                                  .append(")")
+                                  .get()
+                              : doSwitch(varRef))
+                  .collect(new Joiner(", ")));
+    }
+    msb.append(" ").append(doSwitch(object.getCode()));
+    if (object.getStp() != null) msb.append(" ").append(doSwitch(object.getStp()));
+    if (object.getDeadline() != null) msb.append(" ").append(doSwitch(object.getDeadline()));
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseTriggerRef(TriggerRef object) {
+    // BuiltinTriggerRef | VarRef
+    throw new UnsupportedOperationException(
+        "TriggerRefs are BuiltinTriggerRefs or VarRefs, so the methods "
+            + "corresponding to those types should be invoked instead.");
+  }
+
+  @Override
+  public MalleableString caseBuiltinTriggerRef(BuiltinTriggerRef object) {
+    // type = BuiltinTrigger
+    return MalleableString.anyOf(object.getType().getLiteral());
+  }
+
+  @Override
+  public MalleableString caseDeadline(Deadline object) {
+    // 'deadline' '(' delay=Expression ')' code=Code
+    return handler(object, "deadline", Deadline::getDelay, Deadline::getCode);
+  }
+
+  @Override
+  public MalleableString caseSTP(STP object) {
+    // 'STP' '(' value=Expression ')' code=Code
+    return handler(object, "STP", STP::getValue, STP::getCode);
+  }
+
+  private <T extends EObject> MalleableString handler(
+      T object, String name, Function<T, Expression> getTrigger, Function<T, Code> getCode) {
+    return new Builder()
+        .append(name)
+        .append(list(false, getTrigger.apply(object)))
+        .append(" ")
+        .append(doSwitch(getCode.apply(object)))
+        .get();
+  }
+
+  @Override
+  public MalleableString casePreamble(Preamble object) {
+    // (visibility=Visibility)? 'preamble' code=Code
+    Builder msb = new Builder();
+    if (object.getVisibility() != null && object.getVisibility() != Visibility.NONE) {
+      msb.append(object.getVisibility().getLiteral()).append(" ");
+    }
+    return msb.append("preamble ").append(doSwitch(object.getCode())).get();
+  }
+
+  @Override
+  public MalleableString caseInstantiation(Instantiation object) {
+    // name=ID '=' 'new' (widthSpec=WidthSpec)?
+    // reactorClass=[ReactorDecl] ('<' typeParms+=TypeParm (',' typeParms+=TypeParm)* '>')? '('
+    // (parameters+=Assignment (',' parameters+=Assignment)*)?
+    // ')' ('at' host=Host)? ';'?;
+    Builder msb = new Builder();
+    msb.append(object.getName()).append(" = new");
+    if (object.getWidthSpec() != null) msb.append(doSwitch(object.getWidthSpec()));
+    msb.append(" ").append(object.getReactorClass().getName());
+    msb.append(list(", ", "<", ">", true, false, object.getTypeParms()));
+    msb.append(list(false, object.getParameters()));
+    // TODO: Delete the following case when the corresponding feature is removed
+    if (object.getHost() != null) msb.append(" at ").append(doSwitch(object.getHost()));
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseConnection(Connection object) {
+    // ((leftPorts += VarRef (',' leftPorts += VarRef)*)
+    //     | ( '(' leftPorts += VarRef (',' leftPorts += VarRef)* ')' iterated ?= '+'?))
+    // ('->' | physical?='~>')
+    // rightPorts += VarRef (',' rightPorts += VarRef)*
+    //     ('after' delay=Expression)?
+    // (serializer=Serializer)?
+    // ';'?
+    Builder msb = new Builder();
+    Builder left = new Builder();
+    Builder right = new Builder();
+    if (object.isIterated()) {
+      left.append(list(false, object.getLeftPorts())).append("+");
+    } else {
+      left.append(
+          object.getLeftPorts().stream().map(this::doSwitch).collect(new Joiner(", ")),
+          object.getLeftPorts().stream()
+              .map(this::doSwitch)
+              .collect(new Joiner(String.format(",%n"))));
+    }
+    String arrow = object.isPhysical() ? "~>" : "->";
+    right.append(minimallyDelimitedList(object.getRightPorts()));
+    msb.append(
+        MalleableString.anyOf(
             new Builder()
-                .append(String.format("%n"))
-                .append(
-                    list(String.format(",%n"), "", "", true, true, items)
-                        .indent()
-                ).get()
-        );
-    }
-
-    @Override
-    public MalleableString caseSerializer(Serializer object) {
-        // 'serializer' type=STRING
-        return new Builder()
-            .append("serializer \"")
-            .append(object.getType())
-            .append("\"")
-            .get();
-    }
-
-    @Override
-    public MalleableString caseKeyValuePairs(KeyValuePairs object) {
-        // {KeyValuePairs} '{' (pairs+=KeyValuePair (',' (pairs+=KeyValuePair))* ','?)? '}'
-        if (object.getPairs().isEmpty()) return MalleableString.anyOf("");
-        return new Builder()
-            .append("{\n")
-            .append(list(
-                ",\n",
-                "",
-                "\n",
-                true,
-                true,
-                object.getPairs()
-            ).indent())
-            .append("}")
-            .get();
-    }
-
-    @Override
-    public MalleableString caseKeyValuePair(KeyValuePair object) {
-        // name=Kebab ':' value=Element
-        return new Builder()
-            .append(object.getName())
-            .append(": ")
-            .append(doSwitch(object.getValue()))
-            .get();
-    }
-
-    @Override
-    public MalleableString caseArray(Array object) {
-        // '[' elements+=Element (',' (elements+=Element))* ','? ']'
-        return list(", ", "[", "]", false, false, object.getElements());
-    }
-
-    @Override
-    public MalleableString caseElement(Element object) {
-        // keyvalue=KeyValuePairs
-        // | array=Array
-        // | literal=Literal
-        // | (time=INT unit=TimeUnit)
-        // | id=Path
-        if (object.getKeyvalue() != null) return doSwitch(object.getKeyvalue());
-        if (object.getArray() != null) return doSwitch(object.getArray());
-        if (object.getLiteral() != null) return MalleableString.anyOf(object.getLiteral());
-        if (object.getId() != null) return MalleableString.anyOf(object.getId());
-        if (object.getUnit() != null) return MalleableString.anyOf(
-            String.format("%d %s", object.getTime(), object.getUnit())
-        );
-        return MalleableString.anyOf(String.valueOf(object.getTime()));
-    }
-
-    @Override
-    public MalleableString caseTypedVariable(TypedVariable object) {
-        // Port | Action
-        return defaultCase(object);
-    }
-
-    @Override
-    public MalleableString caseVariable(Variable object) {
-        // TypedVariable | Timer | Mode
-        return defaultCase(object);
-    }
-
-    @Override
-    public MalleableString caseAssignment(Assignment object) {
-        // (lhs=[Parameter] (
-        //     (equals='=' rhs+=Expression)
-        //     | ((equals='=')? (
-        //         parens+='(' (rhs+=Expression (',' rhs+=Expression)*)? parens+=')'
-        //         | braces+='{' (rhs+=Expression (',' rhs+=Expression)*)? braces+='}'))
-        // ));
-        Builder msb = new Builder();
-        msb.append(object.getLhs().getName());
-        if (object.getEquals() != null) msb.append(" = ");
-        String prefix = "";
-        String suffix = "";
-        if (!object.getParens().isEmpty()) {
-            prefix = "(";
-            suffix = ")";
-        } else if (!object.getBraces().isEmpty()) {
-            prefix = "{";
-            suffix = "}";
-        }
-        msb.append(list(", ", prefix, suffix, false, prefix.isBlank(), object.getRhs()));
-        return msb.get();
-    }
-
-    @Override
-    public MalleableString caseParameter(Parameter object) {
-        // name=ID (':' (type=Type))?
-        // ((parens+='(' (init+=Expression (','  init+=Expression)*)? parens+=')')
-        // | (braces+='{' (init+=Expression (','  init+=Expression)*)? braces+='}')
-        // )?
-        var builder = new Builder();
-        addAttributes(builder, object::getAttributes);
-        return builder
-            .append(object.getName())
-            .append(typeAnnotationFor(object.getType()))
-            .append(list(
+                .append(left.get())
+                .append(MalleableString.anyOf(" " + arrow))
+                .append(right.get())
+                .get(),
+            new Builder()
+                .append(left.get())
+                .append(MalleableString.anyOf("\n" + arrow).indent())
+                .append(right.get())
+                .get()));
+    if (object.getDelay() != null) msb.append(" after ").append(doSwitch(object.getDelay()));
+    if (object.getSerializer() != null) {
+      msb.append(" ").append(doSwitch(object.getSerializer()));
+    }
+    return msb.get();
+  }
+
+  private MalleableString minimallyDelimitedList(List<? extends EObject> items) {
+    return MalleableString.anyOf(
+        list(", ", " ", "", true, true, items),
+        new Builder()
+            .append(String.format("%n"))
+            .append(list(String.format(",%n"), "", "", true, true, items).indent())
+            .get());
+  }
+
+  @Override
+  public MalleableString caseSerializer(Serializer object) {
+    // 'serializer' type=STRING
+    return new Builder().append("serializer \"").append(object.getType()).append("\"").get();
+  }
+
+  @Override
+  public MalleableString caseKeyValuePairs(KeyValuePairs object) {
+    // {KeyValuePairs} '{' (pairs+=KeyValuePair (',' (pairs+=KeyValuePair))* ','?)? '}'
+    if (object.getPairs().isEmpty()) return MalleableString.anyOf("");
+    return new Builder()
+        .append("{\n")
+        .append(list(",\n", "", "\n", true, true, object.getPairs()).indent())
+        .append("}")
+        .get();
+  }
+
+  @Override
+  public MalleableString caseKeyValuePair(KeyValuePair object) {
+    // name=Kebab ':' value=Element
+    return new Builder()
+        .append(object.getName())
+        .append(": ")
+        .append(doSwitch(object.getValue()))
+        .get();
+  }
+
+  @Override
+  public MalleableString caseArray(Array object) {
+    // '[' elements+=Element (',' (elements+=Element))* ','? ']'
+    return list(", ", "[", "]", false, false, object.getElements());
+  }
+
+  @Override
+  public MalleableString caseElement(Element object) {
+    // keyvalue=KeyValuePairs
+    // | array=Array
+    // | literal=Literal
+    // | (time=INT unit=TimeUnit)
+    // | id=Path
+    if (object.getKeyvalue() != null) return doSwitch(object.getKeyvalue());
+    if (object.getArray() != null) return doSwitch(object.getArray());
+    if (object.getLiteral() != null) return MalleableString.anyOf(object.getLiteral());
+    if (object.getId() != null) return MalleableString.anyOf(object.getId());
+    if (object.getUnit() != null)
+      return MalleableString.anyOf(String.format("%d %s", object.getTime(), object.getUnit()));
+    return MalleableString.anyOf(String.valueOf(object.getTime()));
+  }
+
+  @Override
+  public MalleableString caseTypedVariable(TypedVariable object) {
+    // Port | Action
+    return defaultCase(object);
+  }
+
+  @Override
+  public MalleableString caseVariable(Variable object) {
+    // TypedVariable | Timer | Mode
+    return defaultCase(object);
+  }
+
+  @Override
+  public MalleableString caseAssignment(Assignment object) {
+    // (lhs=[Parameter] (
+    //     (equals='=' rhs+=Expression)
+    //     | ((equals='=')? (
+    //         parens+='(' (rhs+=Expression (',' rhs+=Expression)*)? parens+=')'
+    //         | braces+='{' (rhs+=Expression (',' rhs+=Expression)*)? braces+='}'))
+    // ));
+    Builder msb = new Builder();
+    msb.append(object.getLhs().getName());
+    if (object.getEquals() != null) msb.append(" = ");
+    String prefix = "";
+    String suffix = "";
+    if (!object.getParens().isEmpty()) {
+      prefix = "(";
+      suffix = ")";
+    } else if (!object.getBraces().isEmpty()) {
+      prefix = "{";
+      suffix = "}";
+    }
+    msb.append(list(", ", prefix, suffix, false, prefix.isBlank(), object.getRhs()));
+    return msb.get();
+  }
+
+  @Override
+  public MalleableString caseParameter(Parameter object) {
+    // name=ID (':' (type=Type))?
+    // ((parens+='(' (init+=Expression (','  init+=Expression)*)? parens+=')')
+    // | (braces+='{' (init+=Expression (','  init+=Expression)*)? braces+='}')
+    // )?
+    var builder = new Builder();
+    addAttributes(builder, object::getAttributes);
+    return builder
+        .append(object.getName())
+        .append(typeAnnotationFor(object.getType()))
+        .append(
+            list(
                 ", ",
                 object.getBraces().isEmpty() ? "(" : "{",
                 object.getBraces().isEmpty() ? ")" : "}",
                 true,
                 false,
-                object.getInit()
-            ))
-            .get();
-    }
-
-    @Override
-    public MalleableString caseExpression(Expression object) {
-        // {Literal} literal = Literal
-        // | Time
-        // | ParameterReference
-        // | Code
-        return defaultCase(object);
-    }
-
-    @Override
-    public MalleableString casePort(Port object) {
-        // Input | Output
-        return defaultCase(object);
-    }
-
-    @Override
-    public MalleableString caseWidthSpec(WidthSpec object) {
-        // ofVariableLength?='[]' | '[' (terms+=WidthTerm) ('+' terms+=WidthTerm)* ']';
-        if (object.isOfVariableLength()) return MalleableString.anyOf("[]");
-        return list(" + ", "[", "]", false, false, object.getTerms());
-    }
-
-    @Override
-    public MalleableString caseWidthTerm(WidthTerm object) {
-        // width=INT
-        // | parameter=[Parameter]
-        // | 'widthof(' port=VarRef ')'
-        // | code=Code;
-        if (object.getWidth() != 0) {
-            return MalleableString.anyOf(object.getWidth());
-        } else if (object.getParameter() != null) {
-            return MalleableString.anyOf(object.getParameter().getName());
-        } else if (object.getPort() != null) {
-            return new Builder()
-                .append("widthof")
-                .append(list(false, object.getPort()))
-                .get();
-        } else if (object.getCode() != null) {
-            return doSwitch(object.getCode());
-        }
-        throw new IllegalArgumentException("A WidthTerm should not be totally nullish/zeroish.");
-    }
-
-    @Override
-    public MalleableString caseIPV4Host(IPV4Host object) {
-        // (user=Kebab '@')? addr=IPV4Addr (':' port=INT)?
-        return caseHost(object);
-    }
-
-    @Override
-    public MalleableString caseIPV6Host(IPV6Host object) {
-        // ('[' (user=Kebab '@')? addr=IPV6Addr ']' (':' port=INT)?)
-        return caseHost(object);
-    }
-
-    @Override
-    public MalleableString caseNamedHost(NamedHost object) {
-        // (user=Kebab '@')? addr=HostName (':' port=INT)?
-        return caseHost(object);
-    }
-
-    @Override
-    public MalleableString defaultCase(EObject object) {
-        throw new UnsupportedOperationException(String.format(
+                object.getInit()))
+        .get();
+  }
+
+  @Override
+  public MalleableString caseExpression(Expression object) {
+    // {Literal} literal = Literal
+    // | Time
+    // | ParameterReference
+    // | Code
+    return defaultCase(object);
+  }
+
+  @Override
+  public MalleableString casePort(Port object) {
+    // Input | Output
+    return defaultCase(object);
+  }
+
+  @Override
+  public MalleableString caseWidthSpec(WidthSpec object) {
+    // ofVariableLength?='[]' | '[' (terms+=WidthTerm) ('+' terms+=WidthTerm)* ']';
+    if (object.isOfVariableLength()) return MalleableString.anyOf("[]");
+    return list(" + ", "[", "]", false, false, object.getTerms());
+  }
+
+  @Override
+  public MalleableString caseWidthTerm(WidthTerm object) {
+    // width=INT
+    // | parameter=[Parameter]
+    // | 'widthof(' port=VarRef ')'
+    // | code=Code;
+    if (object.getWidth() != 0) {
+      return MalleableString.anyOf(object.getWidth());
+    } else if (object.getParameter() != null) {
+      return MalleableString.anyOf(object.getParameter().getName());
+    } else if (object.getPort() != null) {
+      return new Builder().append("widthof").append(list(false, object.getPort())).get();
+    } else if (object.getCode() != null) {
+      return doSwitch(object.getCode());
+    }
+    throw new IllegalArgumentException("A WidthTerm should not be totally nullish/zeroish.");
+  }
+
+  @Override
+  public MalleableString caseIPV4Host(IPV4Host object) {
+    // (user=Kebab '@')? addr=IPV4Addr (':' port=INT)?
+    return caseHost(object);
+  }
+
+  @Override
+  public MalleableString caseIPV6Host(IPV6Host object) {
+    // ('[' (user=Kebab '@')? addr=IPV6Addr ']' (':' port=INT)?)
+    return caseHost(object);
+  }
+
+  @Override
+  public MalleableString caseNamedHost(NamedHost object) {
+    // (user=Kebab '@')? addr=HostName (':' port=INT)?
+    return caseHost(object);
+  }
+
+  @Override
+  public MalleableString defaultCase(EObject object) {
+    throw new UnsupportedOperationException(
+        String.format(
             "ToText has no case for %s or any of its supertypes, or it does have such a case, but "
                 + "the return value of that case was null.",
-            object.getClass().getName()
-        ));
-    }
-
-    /**
-     * Represent the given EList as a string.
-     * @param suffix The token marking the end of the list.
-     * @param separator The separator separating elements of the list.
-     * @param prefix The token marking the start of the list.
-     * @param nothingIfEmpty Whether the result should be simplified to the
-     * empty string as opposed to just the prefix and suffix.
-     * @param whitespaceRigid Whether any whitespace appearing in the
-     */
-    private <E extends EObject> MalleableString list(
-        String separator,
-        String prefix,
-        String suffix,
-        boolean nothingIfEmpty,
-        boolean whitespaceRigid,
-        List<E> items
-    ) {
-        return list(
-            separator,
-            prefix,
-            suffix,
-            nothingIfEmpty,
-            whitespaceRigid,
-            (Object[]) items.toArray(EObject[]::new)
-        );
-    }
-
-    private MalleableString list(
-        String separator,
-        String prefix,
-        String suffix,
-        boolean nothingIfEmpty,
-        boolean whitespaceRigid,
-        Object... items
-    ) {
-        if (nothingIfEmpty && Arrays.stream(items).allMatch(Objects::isNull)) {
-            return MalleableString.anyOf("");
-        }
-        MalleableString rigid = Arrays.stream(items).sequential()
+            object.getClass().getName()));
+  }
+
+  /**
+   * Represent the given EList as a string.
+   *
+   * @param suffix The token marking the end of the list.
+   * @param separator The separator separating elements of the list.
+   * @param prefix The token marking the start of the list.
+   * @param nothingIfEmpty Whether the result should be simplified to the empty string as opposed to
+   *     just the prefix and suffix.
+   * @param whitespaceRigid Whether any whitespace appearing in the
+   */
+  private <E extends EObject> MalleableString list(
+      String separator,
+      String prefix,
+      String suffix,
+      boolean nothingIfEmpty,
+      boolean whitespaceRigid,
+      List<E> items) {
+    return list(
+        separator,
+        prefix,
+        suffix,
+        nothingIfEmpty,
+        whitespaceRigid,
+        (Object[]) items.toArray(EObject[]::new));
+  }
+
+  private MalleableString list(
+      String separator,
+      String prefix,
+      String suffix,
+      boolean nothingIfEmpty,
+      boolean whitespaceRigid,
+      Object... items) {
+    if (nothingIfEmpty && Arrays.stream(items).allMatch(Objects::isNull)) {
+      return MalleableString.anyOf("");
+    }
+    MalleableString rigid =
+        Arrays.stream(items)
+            .sequential()
             .filter(Objects::nonNull)
-            .map(it -> {
-                if (it instanceof MalleableString ms) return ms;
-                if (it instanceof EObject eObject) return doSwitch(eObject);
-                return MalleableString.anyOf(Objects.toString(it));
-            }).collect(new Joiner(separator, prefix, suffix));
-        if (whitespaceRigid) return rigid;
-        return MalleableString.anyOf(
-            rigid,
-            new Builder()
-                .append(prefix.stripTrailing() + "\n")
-                .append(list(
-                    separator.strip() + "\n",
-                    "",
-                    "\n",
-                    nothingIfEmpty,
-                    true,
-                    items
-                ).indent())
-                .append(suffix.stripLeading())
-                .get()
-        );
-    }
-
-    private <E extends EObject> MalleableString list(boolean nothingIfEmpty, EList<E> items) {
-        return list(", ", "(", ")", nothingIfEmpty, false, items);
-    }
-
-    private MalleableString list(boolean nothingIfEmpty, Object... items) {
-        return list(", ", "(", ")", nothingIfEmpty, false, items);
-    }
-
-    private MalleableString typeAnnotationFor(Type type) {
-        if (type == null) return MalleableString.anyOf("");
-        return new Builder().append(": ").append(doSwitch(type)).get();
-    }
-
-    private void addAttributes(Builder builder, Supplier<EList<? extends EObject>> getAttributes) {
-        if (getAttributes.get() == null) return;
-        for (var attribute : getAttributes.get()) {
-            builder.append(doSwitch(attribute)).append("\n");
-        }
-    }
-
-    /**
-     * Represent a list of groups of statements.
-     * @param statementListList A list of groups of statements.
-     * @param forceWhitespace Whether to force a line of vertical whitespace
-     * regardless of textual input
-     * @return A string representation of {@code statementListList}.
-     */
-    private MalleableString indentedStatements(
-        List<EList<? extends EObject>> statementListList,
-        boolean forceWhitespace
-    ) {
-        var sorted = statementListList.stream()
+            .map(
+                it -> {
+                  if (it instanceof MalleableString ms) return ms;
+                  if (it instanceof EObject eObject) return doSwitch(eObject);
+                  return MalleableString.anyOf(Objects.toString(it));
+                })
+            .collect(new Joiner(separator, prefix, suffix));
+    if (whitespaceRigid) return rigid;
+    return MalleableString.anyOf(
+        rigid,
+        new Builder()
+            .append(prefix.stripTrailing() + "\n")
+            .append(list(separator.strip() + "\n", "", "\n", nothingIfEmpty, true, items).indent())
+            .append(suffix.stripLeading())
+            .get());
+  }
+
+  private <E extends EObject> MalleableString list(boolean nothingIfEmpty, EList<E> items) {
+    return list(", ", "(", ")", nothingIfEmpty, false, items);
+  }
+
+  private MalleableString list(boolean nothingIfEmpty, Object... items) {
+    return list(", ", "(", ")", nothingIfEmpty, false, items);
+  }
+
+  private MalleableString typeAnnotationFor(Type type) {
+    if (type == null) return MalleableString.anyOf("");
+    return new Builder().append(": ").append(doSwitch(type)).get();
+  }
+
+  private void addAttributes(Builder builder, Supplier<EList<? extends EObject>> getAttributes) {
+    if (getAttributes.get() == null) return;
+    for (var attribute : getAttributes.get()) {
+      builder.append(doSwitch(attribute)).append("\n");
+    }
+  }
+
+  /**
+   * Represent a list of groups of statements.
+   *
+   * @param statementListList A list of groups of statements.
+   * @param forceWhitespace Whether to force a line of vertical whitespace regardless of textual
+   *     input
+   * @return A string representation of {@code statementListList}.
+   */
+  private MalleableString indentedStatements(
+      List<EList<? extends EObject>> statementListList, boolean forceWhitespace) {
+    var sorted =
+        statementListList.stream()
             .flatMap(List::stream)
             .sorted(Comparator.comparing(object -> NodeModelUtils.getNode(object).getStartLine()))
             .toList();
-        if (sorted.isEmpty()) return MalleableString.anyOf("");
-        var ret = new Builder();
-        var first = true;
-        for (var object : sorted) {
-            if (!first) {
-                INode node = NodeModelUtils.getNode(object);
-                StringBuilder leadingText = new StringBuilder();
-                if (!forceWhitespace) {
-                    for (INode n : node.getAsTreeIterable()) {
-                        if (n instanceof ICompositeNode) continue;
-                        if (!ASTUtils.isComment(n) && !n.getText().isBlank()) break;
-                        leadingText.append(n.getText());
-                    }
-                }
-                boolean hasLeadingBlankLines = leadingText.toString().lines()
-                    .skip(1)
-                    .filter(String::isBlank).count() > 1;
-                ret.append("\n".repeat(
-                    forceWhitespace || hasLeadingBlankLines ? 2 : 1
-                ));
-            }
-            ret.append(doSwitch(object));
-            first = false;
+    if (sorted.isEmpty()) return MalleableString.anyOf("");
+    var ret = new Builder();
+    var first = true;
+    for (var object : sorted) {
+      if (!first) {
+        INode node = NodeModelUtils.getNode(object);
+        StringBuilder leadingText = new StringBuilder();
+        if (!forceWhitespace) {
+          for (INode n : node.getAsTreeIterable()) {
+            if (n instanceof ICompositeNode) continue;
+            if (!ASTUtils.isComment(n) && !n.getText().isBlank()) break;
+            leadingText.append(n.getText());
+          }
         }
-        return ret.append("\n").get().indent();
-    }
+        boolean hasLeadingBlankLines =
+            leadingText.toString().lines().skip(1).filter(String::isBlank).count() > 1;
+        ret.append("\n".repeat(forceWhitespace || hasLeadingBlankLines ? 2 : 1));
+      }
+      ret.append(doSwitch(object));
+      first = false;
+    }
+    return ret.append("\n").get().indent();
+  }
 }
diff --git a/test/C/src/IdentifierLength.lf b/test/C/src/IdentifierLength.lf
index d08ad86c1..69f1fcfd7 100644
--- a/test/C/src/IdentifierLength.lf
+++ b/test/C/src/IdentifierLength.lf
@@ -37,6 +37,6 @@ main reactor IdentifierLength {
     another_really_long_name_for_a_test_instance = new Another_Really_Long_Name_For_A_Test_Class(
 
     )
-    a_really_long_name_for_a_source_instance.y ->
-        another_really_long_name_for_a_test_instance.x
+    a_really_long_name_for_a_source_instance.y
+        -> another_really_long_name_for_a_test_instance.x
 }
diff --git a/test/C/src/federated/DistributedLogicalActionUpstreamLong.lf b/test/C/src/federated/DistributedLogicalActionUpstreamLong.lf
index a37479acf..253112370 100644
--- a/test/C/src/federated/DistributedLogicalActionUpstreamLong.lf
+++ b/test/C/src/federated/DistributedLogicalActionUpstreamLong.lf
@@ -46,7 +46,8 @@ federated reactor {
     passThroughs7.out,
     passThroughs8.out,
     passThroughs9.out,
-    passThroughs10.out ->
+    passThroughs10.out
+        ->
         passThroughs1.in,
         passThroughs2.in,
         passThroughs3.in,
diff --git a/test/C/src/federated/DistributedPhysicalActionUpstreamLong.lf b/test/C/src/federated/DistributedPhysicalActionUpstreamLong.lf
index 7bdb40b07..3830a3e53 100644
--- a/test/C/src/federated/DistributedPhysicalActionUpstreamLong.lf
+++ b/test/C/src/federated/DistributedPhysicalActionUpstreamLong.lf
@@ -66,7 +66,8 @@ federated reactor {
     passThroughs7.out,
     passThroughs8.out,
     passThroughs9.out,
-    passThroughs10.out ->
+    passThroughs10.out
+        ->
         passThroughs1.in,
         passThroughs2.in,
         passThroughs3.in,
diff --git a/test/C/src/modal_models/BanksCount3ModesComplex.lf b/test/C/src/modal_models/BanksCount3ModesComplex.lf
index 2f28036d8..ad08cc732 100644
--- a/test/C/src/modal_models/BanksCount3ModesComplex.lf
+++ b/test/C/src/modal_models/BanksCount3ModesComplex.lf
@@ -71,8 +71,8 @@ main reactor {
             250000000,1,1,1,1,1,1,1,1,0,3,0,3,0,3,0,3,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0
         ), training = false)
 
-    counters.always, counters.mode1, counters.mode2, counters.never ->
-        test.events
+    counters.always, counters.mode1, counters.mode2, counters.never
+        -> test.events
 
     reaction(stepper) -> counters.next {=  // Trigger
         for(int i = 0; i < 2; i++) {
diff --git a/test/C/src/modal_models/BanksModalStateReset.lf b/test/C/src/modal_models/BanksModalStateReset.lf
index 202d6be5b..1dde9c5b9 100644
--- a/test/C/src/modal_models/BanksModalStateReset.lf
+++ b/test/C/src/modal_models/BanksModalStateReset.lf
@@ -45,8 +45,8 @@ main reactor {
     reset2.mode_switch,
     reset2.count0,
     reset2.count1,
-    reset2.count2 ->
-        test.events
+    reset2.count2
+        -> test.events
 
     // Trigger mode change (separately because of #1278)
     reaction(stepper) -> reset1.next {=
diff --git a/test/C/src/modal_models/ModalActions.lf b/test/C/src/modal_models/ModalActions.lf
index b1894a26d..1d8dff8cb 100644
--- a/test/C/src/modal_models/ModalActions.lf
+++ b/test/C/src/modal_models/ModalActions.lf
@@ -92,8 +92,8 @@ main reactor {
     modal.action1_sched,
     modal.action1_exec,
     modal.action2_sched,
-    modal.action2_exec ->
-        test.events
+    modal.action2_exec
+        -> test.events
 
     // Trigger mode change
     reaction(stepper) -> modal.next {= lf_set(modal.next, true); =}
diff --git a/test/C/src/modal_models/ModalAfter.lf b/test/C/src/modal_models/ModalAfter.lf
index bbc810053..038a368cc 100644
--- a/test/C/src/modal_models/ModalAfter.lf
+++ b/test/C/src/modal_models/ModalAfter.lf
@@ -97,8 +97,8 @@ main reactor {
     modal.produced1,
     modal.consumed1,
     modal.produced2,
-    modal.consumed2 ->
-        test.events
+    modal.consumed2
+        -> test.events
 
     // Trigger mode change
     reaction(stepper) -> modal.next {= lf_set(modal.next, true); =}
diff --git a/test/C/src/modal_models/ModalStartupShutdown.lf b/test/C/src/modal_models/ModalStartupShutdown.lf
index f452efad0..fa1ebaacd 100644
--- a/test/C/src/modal_models/ModalStartupShutdown.lf
+++ b/test/C/src/modal_models/ModalStartupShutdown.lf
@@ -139,8 +139,8 @@ main reactor {
     modal.shutdown4,
     modal.startup5,
     modal.reset5,
-    modal.shutdown5 ->
-        test.events
+    modal.shutdown5
+        -> test.events
 
     // Trigger mode change
     reaction(stepper) -> modal.next {= lf_set(modal.next, true); =}
diff --git a/test/Python/src/IdentifierLength.lf b/test/Python/src/IdentifierLength.lf
index 5e7090bf1..66edf11ab 100644
--- a/test/Python/src/IdentifierLength.lf
+++ b/test/Python/src/IdentifierLength.lf
@@ -38,6 +38,6 @@ main reactor IdentifierLength {
     another_really_long_name_for_a_test_instance = new Another_Really_Long_Name_For_A_Test_Class(
 
     )
-    a_really_long_name_for_a_source_instance.y ->
-        another_really_long_name_for_a_test_instance.x
+    a_really_long_name_for_a_source_instance.y
+        -> another_really_long_name_for_a_test_instance.x
 }
diff --git a/test/Python/src/modal_models/BanksCount3ModesComplex.lf b/test/Python/src/modal_models/BanksCount3ModesComplex.lf
index d66d489bd..59b41ef75 100644
--- a/test/Python/src/modal_models/BanksCount3ModesComplex.lf
+++ b/test/Python/src/modal_models/BanksCount3ModesComplex.lf
@@ -68,8 +68,8 @@ main reactor {
         250000000,1,1,1,1,1,1,1,1,0,3,0,3,0,3,0,3,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0
     ), training = False)
 
-    counters.always, counters.mode1, counters.mode2, counters.never ->
-        test.events
+    counters.always, counters.mode1, counters.mode2, counters.never
+        -> test.events
 
     reaction(stepper) -> counters.next {=  # Trigger
         for i in range(len(counters)):
diff --git a/test/Python/src/modal_models/BanksModalStateReset.lf b/test/Python/src/modal_models/BanksModalStateReset.lf
index ecfbd057c..f96a7bd63 100644
--- a/test/Python/src/modal_models/BanksModalStateReset.lf
+++ b/test/Python/src/modal_models/BanksModalStateReset.lf
@@ -45,8 +45,8 @@ main reactor {
     reset2.mode_switch,
     reset2.count0,
     reset2.count1,
-    reset2.count2 ->
-        test.events
+    reset2.count2
+        -> test.events
 
     # Trigger mode change (separately because of #1278)
     reaction(stepper) -> reset1.next {=
diff --git a/test/Python/src/modal_models/ModalActions.lf b/test/Python/src/modal_models/ModalActions.lf
index 5d9ca1099..b74f92f3c 100644
--- a/test/Python/src/modal_models/ModalActions.lf
+++ b/test/Python/src/modal_models/ModalActions.lf
@@ -87,8 +87,8 @@ main reactor {
     modal.action1_sched,
     modal.action1_exec,
     modal.action2_sched,
-    modal.action2_exec ->
-        test.events
+    modal.action2_exec
+        -> test.events
 
     # Trigger mode change
     reaction(stepper) -> modal.next {= modal.next.set(True) =}
diff --git a/test/Python/src/modal_models/ModalAfter.lf b/test/Python/src/modal_models/ModalAfter.lf
index 820cd5021..c86002c9f 100644
--- a/test/Python/src/modal_models/ModalAfter.lf
+++ b/test/Python/src/modal_models/ModalAfter.lf
@@ -92,8 +92,8 @@ main reactor {
     modal.produced1,
     modal.consumed1,
     modal.produced2,
-    modal.consumed2 ->
-        test.events
+    modal.consumed2
+        -> test.events
 
     # Trigger mode change
     reaction(stepper) -> modal.next {= modal.next.set(True) =}
diff --git a/test/Python/src/modal_models/ModalStartupShutdown.lf b/test/Python/src/modal_models/ModalStartupShutdown.lf
index 8407189e0..c7a868eec 100644
--- a/test/Python/src/modal_models/ModalStartupShutdown.lf
+++ b/test/Python/src/modal_models/ModalStartupShutdown.lf
@@ -134,8 +134,8 @@ main reactor {
     modal.shutdown4,
     modal.startup5,
     modal.reset5,
-    modal.shutdown5 ->
-        test.events
+    modal.shutdown5
+        -> test.events
 
     # Trigger mode change
     reaction(stepper) -> modal.next {= modal.next.set(True) =}
['test/C/src/modal_models/BanksCount3ModesComplex.lf', 'test/Python/src/modal_models/BanksModalStateReset.lf', 'test/Python/src/modal_models/ModalAfter.lf', 'org.lflang/src/org/lflang/ast/MalleableString.java', 'test/C/src/federated/DistributedLogicalActionUpstreamLong.lf', 'test/Python/src/modal_models/BanksCount3ModesComplex.lf', 'test/C/src/IdentifierLength.lf', 'org.lflang/src/org/lflang/ast/ToLf.java', 'test/C/src/modal_models/ModalAfter.lf', 'test/C/src/modal_models/BanksModalStateReset.lf', 'test/C/src/modal_models/ModalStartupShutdown.lf', 'test/C/src/federated/DistributedPhysicalActionUpstreamLong.lf', 'test/C/src/modal_models/ModalActions.lf', 'test/Python/src/modal_models/ModalStartupShutdown.lf', 'test/Python/src/modal_models/ModalActions.lf', 'test/Python/src/IdentifierLength.lf']