Repository: compiler-explorer/compiler-explorer
Pull request #7151 (2024-11-29 05:15:10)

Add preliminary Slang support
Part of https://github.com/compiler-explorer/compiler-explorer/issues/2331 and similar to [my GLSL change](https://github.com/compiler-explorer/compiler-explorer/pull/6883)

[Slang](https://shader-slang.com/) is a GPU focused shading language that has been worked on for years. Last week [The Khronos Group](https://www.khronos.org/news/press/khronos-group-launches-slang-initiative-hosting-open-source-compiler-contributed-by-nvidia) will now be running the project as open governance. The latest [Vulkan SDK](https://www.lunarg.com/lunarg-releases-vulkan-sdk-1-3-296-0-for-windows-linux-macos/) has also added a build of `Slangc` (slang compiler).

This change adds support for Slang (the language) as a front end with `Slangc` (the compiler) as the only compiler. Slang can be used for things like GLSL/HLSL, but that is for a future PR.

I am in contacts with people on the Slang development and plan to support things for Slang as well as the other GPU related shading languages 

--- 

of course, screen shots as well

![image](https://github.com/user-attachments/assets/c9f6460b-b779-4ff5-b0bb-5a7eff543de1)

![image](https://github.com/user-attachments/assets/b2dbdd5a-c031-4110-ab9d-ed76b9881ad8)



Patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 6ea83bdd5..48ba4d683 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -327,6 +327,12 @@
           - 'lib/compilers/scala.ts'
           - 'etc/config/scala.*.properties'
 
+'lang-slang':
+  - changed-files:
+      - any-glob-to-any-file:
+          - 'lib/compilers/slang.ts'
+          - 'etc/config/slang.*.properties'
+
 'lang-solidity':
   - changed-files:
       - any-glob-to-any-file:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index c7ede3ae7..e5c0848e8 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -150,3 +150,4 @@ From oldest to newest contributor, we would like to thank:
 - [Nicholas Hubbard](https://github.com/nhubbard)
 - [Detjon Mataj](https://github.com/detjonmataj)
 - [Pierre Bayerl](https://github.com/goto40)
+- [James Touton](https://github.com/Bekenn)
diff --git a/app.ts b/app.ts
index 12c70e5df..7c805cafd 100755
--- a/app.ts
+++ b/app.ts
@@ -57,11 +57,14 @@ import {startWineInit} from './lib/exec.js';
 import {RemoteExecutionQuery} from './lib/execution/execution-query.js';
 import {initHostSpecialties} from './lib/execution/execution-triple.js';
 import {startExecutionWorkerThread} from './lib/execution/sqs-execution-queue.js';
+import {FormattingService} from './lib/formatting-service.js';
 import {AssemblyDocumentationController} from './lib/handlers/api/assembly-documentation-controller.js';
+import {FormattingController} from './lib/handlers/api/formatting-controller.js';
 import {HealthcheckController} from './lib/handlers/api/healthcheck-controller.js';
 import {SiteTemplateController} from './lib/handlers/api/site-template-controller.js';
 import {SourceController} from './lib/handlers/api/source-controller.js';
 import {CompileHandler} from './lib/handlers/compile.js';
+import {cached, csp} from './lib/handlers/middleware.js';
 import {NoScriptHandler} from './lib/handlers/noscript.js';
 import {RouteAPI, ShortLinkMetaData} from './lib/handlers/route-api.js';
 import {languages as allLanguages} from './lib/languages.js';
@@ -337,23 +340,6 @@ const httpRoot = urljoin(ceProps('httpRoot', '/'), '/');
 const staticUrl = ceProps<string | undefined>('staticUrl');
 const staticRoot = urljoin(staticUrl || urljoin(httpRoot, 'static'), '/');
 
-/**
- * Sets appropriate headers for static resources. This is based on the staticMaxAgeSecs property from the
- * compiler-explorer.properties file.
- */
-export function addStaticHeaders(res: express.Response) {
-    if (staticMaxAgeSecs) {
-        res.setHeader('Cache-Control', `public, max-age=${staticMaxAgeSecs}, must-revalidate`);
-    }
-}
-
-function contentPolicyHeader(res: express.Response) {
-    // TODO: re-enable CSP
-    // if (csp) {
-    //     res.setHeader('Content-Security-Policy', csp);
-    // }
-}
-
 function measureEventLoopLag(delayMs: number) {
     return new Promise<number>(resolve => {
         const start = process.hrtime.bigint();
@@ -545,12 +531,16 @@ async function main() {
 
     RemoteExecutionQuery.initRemoteExecutionArchs(ceProps, defArgs.env);
 
+    const formattingService = new FormattingService();
+    await formattingService.initialize(ceProps);
+
     const clientOptionsHandler = new ClientOptionsHandler(sources, compilerProps, defArgs);
     const compilationQueue = CompilationQueue.fromProps(compilerProps.ceProps);
     const compilationEnvironment = new CompilationEnvironment(
         compilerProps,
         awsProps,
         compilationQueue,
+        formattingService,
         defArgs.doCache,
     );
     const compileHandler = new CompileHandler(compilationEnvironment, awsProps);
@@ -570,6 +560,7 @@ async function main() {
         compileHandler,
         isExecutionWorker,
     );
+    const formattingController = new FormattingController(formattingService);
 
     logger.info('=======================================');
     if (gitReleaseName) logger.info(`  git release ${gitReleaseName}`);
@@ -629,8 +620,6 @@ async function main() {
         defArgs,
         renderConfig,
         renderGoldenLayout,
-        staticHeaders: addStaticHeaders,
-        contentPolicyHeader,
     };
 
     const noscriptHandler = new NoScriptHandler(router, global.handler_config);
@@ -677,6 +666,9 @@ async function main() {
                 ip: true,
             }),
         )
+        // The healthcheck controller is hoisted to prevent it from being logged.
+        // TODO: Migrate the logger to a shared middleware.
+        .use(healthCheckController.createRouter())
         // eslint-disable-next-line no-unused-vars
         .use(
             responseTime((req, res, time) => {
@@ -744,9 +736,6 @@ async function main() {
         req: express.Request,
         res: express.Response,
     ) {
-        addStaticHeaders(res);
-        contentPolicyHeader(res);
-
         const embedded = req.query.embedded === 'true' ? true : false;
 
         res.render(
@@ -765,8 +754,6 @@ async function main() {
     }
 
     const embeddedHandler = function (req: express.Request, res: express.Response) {
-        addStaticHeaders(res);
-        contentPolicyHeader(res);
         res.render(
             'embed',
             renderConfig(
@@ -809,9 +796,7 @@ async function main() {
             }),
         )
         .use(compression())
-        .get('/', (req, res) => {
-            addStaticHeaders(res);
-            contentPolicyHeader(res);
+        .get('/', cached, csp, (req, res) => {
             res.render(
                 'index',
                 renderConfig(
@@ -823,12 +808,10 @@ async function main() {
                 ),
             );
         })
-        .get('/e', embeddedHandler)
+        .get('/e', cached, csp, embeddedHandler)
         // legacy. not a 301 to prevent any redirect loops between old e links and embed.html
-        .get('/embed.html', embeddedHandler)
-        .get('/embed-ro', (req, res) => {
-            addStaticHeaders(res);
-            contentPolicyHeader(res);
+        .get('/embed.html', cached, csp, embeddedHandler)
+        .get('/embed-ro', cached, csp, (req, res) => {
             res.render(
                 'embed',
                 renderConfig(
@@ -841,24 +824,19 @@ async function main() {
                 ),
             );
         })
-        .get('/robots.txt', (req, res) => {
-            addStaticHeaders(res);
+        .get('/robots.txt', cached, (req, res) => {
             res.end('User-agent: *\nSitemap: https://godbolt.org/sitemap.xml\nDisallow:');
         })
-        .get('/sitemap.xml', (req, res) => {
-            addStaticHeaders(res);
+        .get('/sitemap.xml', cached, (req, res) => {
             res.set('Content-Type', 'application/xml');
             res.render('sitemap');
         })
         .use(sFavicon(utils.resolvePathFromAppRoot('static/favicons', getFaviconFilename())))
-        .get('/client-options.js', (req, res) => {
-            addStaticHeaders(res);
+        .get('/client-options.js', cached, (req, res) => {
             res.set('Content-Type', 'application/javascript');
             res.end(`window.compilerExplorerOptions = ${clientOptionsHandler.getJSON()};`);
         })
-        .use('/bits/:bits(\\w+).html', (req, res) => {
-            addStaticHeaders(res);
-            contentPolicyHeader(res);
+        .use('/bits/:bits(\\w+).html', cached, csp, (req, res) => {
             res.render(
                 `bits/${sanitize(req.params.bits)}`,
                 renderConfig(
@@ -874,10 +852,8 @@ async function main() {
         .use(siteTemplateController.createRouter())
         .use(sourceController.createRouter())
         .use(assemblyDocumentationController.createRouter())
-        .use(healthCheckController.createRouter())
-        .get('/g/:id', oldGoogleUrlHandler)
-        // Deprecated old route for this -- TODO remove in late 2021
-        .post('/shortener', routeApi.apiHandler.shortener.handle.bind(routeApi.apiHandler.shortener));
+        .use(formattingController.createRouter())
+        .get('/g/:id', oldGoogleUrlHandler);
 
     noscriptHandler.InitializeRoutes({limit: ceProps('bodyParserLimit', maxUploadSize)});
     routeApi.InitializeRoutes();
diff --git a/etc/config/android-java.amazon.properties b/etc/config/android-java.amazon.properties
index 291557d12..a29ead7d5 100644
--- a/etc/config/android-java.amazon.properties
+++ b/etc/config/android-java.amazon.properties
@@ -1,4 +1,4 @@
-compilers=&android-d8:&android-r8:&dex2oat
+compilers=&android-d8:&android-r8:&dex2oat:&android-openjdk
 
 group.android-d8.compilers=java-d8-8156:java-d8-8172:java-d8-8233:java-d8-8242:java-d8-8247:java-d8-8336:java-d8-8337:java-d8-8510:java-d8-8527:java-d8-8535
 group.android-d8.compilerType=android-d8
@@ -139,6 +139,105 @@ compiler.java-dex2oat-3310.objdumper=/opt/compiler-explorer/dex2oat-33.10/x86_64
 compiler.java-dex2oat-3310.d8Id=java-d8-8242
 compiler.java-dex2oat-3310.profmanPath=/opt/compiler-explorer/dex2oat-33.10/x86_64/bin/profman
 
+group.android-openjdk.compilers=android-openjdk-1102:android-openjdk-1201:android-openjdk-1202:android-openjdk-1302:android-openjdk-1402:android-openjdk-1502:android-openjdk-1601:android-openjdk-1700:android-openjdk-1702:android-openjdk-1800:android-openjdk-1802:android-openjdk-1902:android-openjdk-2000:android-openjdk-2002:android-openjdk-2100:android-openjdk-2102:android-openjdk-2200
+group.android-openjdk.compilerType=java
+group.android-openjdk.versionFlag=-version
+group.android-openjdk.groupName=OpenJDK
+group.android-openjdk.demangler=
+group.android-openjdk.postProcess=
+group.android-openjdk.options=
+group.android-openjdk.supportsBinary=false
+group.android-openjdk.needsMulti=false
+group.android-openjdk.supportsExecute=true
+group.android-openjdk.interpreted=true
+group.android-openjdk.isSemVer=true
+group.android-openjdk.baseName=jdk
+
+compiler.android-openjdk-2200.exe=/opt/compiler-explorer/jdk-22.0.0/bin/javac
+compiler.android-openjdk-2200.objdumper=/opt/compiler-explorer/jdk-22.0.0/bin/javap
+compiler.android-openjdk-2200.runtime=/opt/compiler-explorer/jdk-22.0.0/bin/java
+compiler.android-openjdk-2200.semver=22.0.0
+
+compiler.android-openjdk-2102.exe=/opt/compiler-explorer/jdk-21.0.2/bin/javac
+compiler.android-openjdk-2102.objdumper=/opt/compiler-explorer/jdk-21.0.2/bin/javap
+compiler.android-openjdk-2102.runtime=/opt/compiler-explorer/jdk-21.0.2/bin/java
+compiler.android-openjdk-2102.semver=21.0.2
+
+compiler.android-openjdk-2100.exe=/opt/compiler-explorer/jdk-21.0.0/bin/javac
+compiler.android-openjdk-2100.objdumper=/opt/compiler-explorer/jdk-21.0.0/bin/javap
+compiler.android-openjdk-2100.runtime=/opt/compiler-explorer/jdk-21.0.0/bin/java
+compiler.android-openjdk-2100.semver=21.0.0
+
+compiler.android-openjdk-2002.exe=/opt/compiler-explorer/jdk-20.0.2/bin/javac
+compiler.android-openjdk-2002.objdumper=/opt/compiler-explorer/jdk-20.0.2/bin/javap
+compiler.android-openjdk-2002.runtime=/opt/compiler-explorer/jdk-20.0.2/bin/java
+compiler.android-openjdk-2002.semver=20.0.2
+
+compiler.android-openjdk-2000.exe=/opt/compiler-explorer/jdk-20.0.0/bin/javac
+compiler.android-openjdk-2000.objdumper=/opt/compiler-explorer/jdk-20.0.0/bin/javap
+compiler.android-openjdk-2000.runtime=/opt/compiler-explorer/jdk-20.0.0/bin/java
+compiler.android-openjdk-2000.semver=20.0.0
+
+compiler.android-openjdk-1902.exe=/opt/compiler-explorer/jdk-19.0.2/bin/javac
+compiler.android-openjdk-1902.objdumper=/opt/compiler-explorer/jdk-19.0.2/bin/javap
+compiler.android-openjdk-1902.runtime=/opt/compiler-explorer/jdk-19.0.2/bin/java
+compiler.android-openjdk-1902.semver=19.0.2
+
+compiler.android-openjdk-1802.exe=/opt/compiler-explorer/jdk-18.0.2/bin/javac
+compiler.android-openjdk-1802.objdumper=/opt/compiler-explorer/jdk-18.0.2/bin/javap
+compiler.android-openjdk-1802.runtime=/opt/compiler-explorer/jdk-18.0.2/bin/java
+compiler.android-openjdk-1802.semver=18.0.2
+
+compiler.android-openjdk-1800.exe=/opt/compiler-explorer/jdk-18.0.0/bin/javac
+compiler.android-openjdk-1800.objdumper=/opt/compiler-explorer/jdk-18.0.0/bin/javap
+compiler.android-openjdk-1800.runtime=/opt/compiler-explorer/jdk-18.0.0/bin/java
+compiler.android-openjdk-1800.semver=18.0.0
+
+compiler.android-openjdk-1702.exe=/opt/compiler-explorer/jdk-17.0.2/bin/javac
+compiler.android-openjdk-1702.objdumper=/opt/compiler-explorer/jdk-17.0.2/bin/javap
+compiler.android-openjdk-1702.runtime=/opt/compiler-explorer/jdk-17.0.2/bin/java
+compiler.android-openjdk-1702.semver=17.0.2
+
+compiler.android-openjdk-1700.exe=/opt/compiler-explorer/jdk-17.0.0/bin/javac
+compiler.android-openjdk-1700.objdumper=/opt/compiler-explorer/jdk-17.0.0/bin/javap
+compiler.android-openjdk-1700.runtime=/opt/compiler-explorer/jdk-17.0.0/bin/java
+compiler.android-openjdk-1700.semver=17.0.0
+
+compiler.android-openjdk-1601.exe=/opt/compiler-explorer/jdk-16.0.1/bin/javac
+compiler.android-openjdk-1601.objdumper=/opt/compiler-explorer/jdk-16.0.1/bin/javap
+compiler.android-openjdk-1601.runtime=/opt/compiler-explorer/jdk-16.0.1/bin/java
+compiler.android-openjdk-1601.semver=16.0.1
+
+compiler.android-openjdk-1502.exe=/opt/compiler-explorer/jdk-15.0.2/bin/javac
+compiler.android-openjdk-1502.objdumper=/opt/compiler-explorer/jdk-15.0.2/bin/javap
+compiler.android-openjdk-1502.runtime=/opt/compiler-explorer/jdk-15.0.2/bin/java
+compiler.android-openjdk-1502.semver=15.0.2
+
+compiler.android-openjdk-1402.exe=/opt/compiler-explorer/jdk-14.0.2/bin/javac
+compiler.android-openjdk-1402.objdumper=/opt/compiler-explorer/jdk-14.0.2/bin/javap
+compiler.android-openjdk-1402.runtime=/opt/compiler-explorer/jdk-14.0.2/bin/java
+compiler.android-openjdk-1402.semver=14.0.2
+
+compiler.android-openjdk-1302.exe=/opt/compiler-explorer/jdk-13.0.2/bin/javac
+compiler.android-openjdk-1302.objdumper=/opt/compiler-explorer/jdk-13.0.2/bin/javap
+compiler.android-openjdk-1302.runtime=/opt/compiler-explorer/jdk-13.0.2/bin/java
+compiler.android-openjdk-1302.semver=13.0.2
+
+compiler.android-openjdk-1202.exe=/opt/compiler-explorer/jdk-12.0.2/bin/javac
+compiler.android-openjdk-1202.objdumper=/opt/compiler-explorer/jdk-12.0.2/bin/javap
+compiler.android-openjdk-1202.runtime=/opt/compiler-explorer/jdk-12.0.2/bin/java
+compiler.android-openjdk-1202.semver=12.0.2
+
+compiler.android-openjdk-1201.exe=/opt/compiler-explorer/jdk-12.0.1/bin/javac
+compiler.android-openjdk-1201.objdumper=/opt/compiler-explorer/jdk-12.0.1/bin/javap
+compiler.android-openjdk-1201.runtime=/opt/compiler-explorer/jdk-12.0.1/bin/java
+compiler.android-openjdk-1201.semver=12.0.1
+
+compiler.android-openjdk-1102.exe=/opt/compiler-explorer/jdk-11.0.2/bin/javac
+compiler.android-openjdk-1102.objdumper=/opt/compiler-explorer/jdk-11.0.2/bin/javap
+compiler.android-openjdk-1102.runtime=/opt/compiler-explorer/jdk-11.0.2/bin/java
+compiler.android-openjdk-1102.semver=11.0.2
+
 defaultCompiler=java-dex2oat-latest
 
 libs=android-api-stubs:r8-keepanno:androidx-annotation
diff --git a/etc/config/c++.amazon.properties b/etc/config/c++.amazon.properties
index cf2810e60..e05b842df 100644
--- a/etc/config/c++.amazon.properties
+++ b/etc/config/c++.amazon.properties
@@ -423,7 +423,7 @@ compiler.clang1910.ldPath=${exePath}/../lib|${exePath}/../lib/x86_64-unknown-lin
 compiler.clang1910.debugPatched=true
 
 
-group.clangx86trunk.compilers=clang_trunk:clang_assertions_trunk:clang_concepts:clang_p1144:clang_autonsdmi:clang_bb_p2996:clang_p3068:clang_p3309:clang_p3367:clang_p3372:clang_p3412:clang_lifetime:clang_p1061:clang_parmexpr:clang_patmat:clang_embed:clang_dang:clang_reflection:clang_variadic_friends:clang_widberg:clang_resugar:clang_clangir:clang_dascandy_contracts:clang_ericwf_contracts:clang_p1974
+group.clangx86trunk.compilers=clang_trunk:clang_assertions_trunk:clang_concepts:clang_p1144:clang_autonsdmi:clang_bb_p2996:clang_p2998:clang_p3068:clang_p3309:clang_p3367:clang_p3372:clang_p3412:clang_lifetime:clang_p1061:clang_parmexpr:clang_patmat:clang_embed:clang_dang:clang_reflection:clang_variadic_friends:clang_widberg:clang_resugar:clang_clangir:clang_dascandy_contracts:clang_ericwf_contracts:clang_p1974
 group.clangx86trunk.intelAsm=-mllvm --x86-asm-syntax=intel
 group.clangx86trunk.options=--gcc-toolchain=/opt/compiler-explorer/gcc-snapshot
 group.clangx86trunk.objdumper=/opt/compiler-explorer/gcc-snapshot/bin/objdump
@@ -466,6 +466,10 @@ compiler.clang_bb_p2996.exe=/opt/compiler-explorer/clang-bb-p2996-trunk/bin/clan
 compiler.clang_bb_p2996.semver=(experimental P2996)
 compiler.clang_bb_p2996.notification=Experimental Reflection Support; see <a href="https://github.com/bloomberg/clang-p2996/blob/p2996/P2996.md" target="_blank" rel="noopener noreferrer">P2996<sup><small class="fas fa-external-link-alt opens-new-window" title="Opens in a new window"></small></sup></a>
 compiler.clang_bb_p2996.options=-std=c++26 -freflection -stdlib=libc++
+compiler.clang_p2998.exe=/opt/compiler-explorer/clang-p2998-trunk/bin/clang++
+compiler.clang_p2998.semver=(experimental P2998)
+compiler.clang_p2998.notification=Experimental support for additional template argument deduction from a function call; see <a href="https://github.com/Bekenn/llvm-project/tree/p2998" target="_blank" rel="noopener noreferrer">P2998<sup><small class="fas fa-external-link-alt opens-new-window" title="Opens in a new window"></small></sup></a>
+compiler.clang_p2998.options=-p2998
 compiler.clang_p3068.exe=/opt/compiler-explorer/clang-p3068-trunk/bin/clang++
 compiler.clang_p3068.semver=(experimental P3068)
 compiler.clang_p3068.notification=Experimental constexpr Exception Support; see <a href="https://github.com/hanickadot/llvm-project/tree/P3068-constexpr-exceptions" target="_blank" rel="noopener noreferrer">P3068<sup><small class="fas fa-external-link-alt opens-new-window" title="Opens in a new window"></small></sup></a>
@@ -1529,6 +1533,11 @@ group.gccbpf.groupName=BPF GCC
 group.gccbpf.isSemVer=true
 group.gccbpf.objdumper=/opt/compiler-explorer/bpf/gcc-trunk/bpf-unknown-none/bin/bpf-unknown-objdump
 
+# hide them all, C++ is not really supported by gccbpf and is more confusing
+# than anything. It was kind of working (by chance) on older gcc, but broken on
+# more recent versions.
+group.gccbpf.hidden=true
+
 compiler.bpfg1310.exe=/opt/compiler-explorer/bpf/gcc-13.1.0/bpf-unknown-none/bin/bpf-unknown-none-g++
 compiler.bpfg1310.semver=13.1.0
 compiler.bpfg1310.objdumper=/opt/compiler-explorer/bpf/gcc-13.1.0/bpf-unknown-none/bin/bpf-unknown-objdump
@@ -3722,7 +3731,7 @@ compiler.nvcxx_x86_cxx24_11.nvdisasm=/opt/compiler-explorer/hpc_sdk/Linux_x86_64
 compiler.nvcxx_x86_cxx24_11.exe=/opt/compiler-explorer/hpc_sdk/Linux_x86_64/24.11/compilers/bin/nvc++
 compiler.nvcxx_x86_cxx24_11.semver=24.11
 
-group.nvcxx_arm_cxx.compilers=nvcxx_arm_cxx22_7:nvcxx_arm_cxx22_9:nvcxx_arm_cxx22_11:nvcxx_arm_cxx23_1:nvcxx_arm_cxx23_3:nvcxx_arm_cxx23_5:nvcxx_arm_cxx23_7:nvcxx_arm_cxx23_9:nvcxx_arm_cxx23_11:nvcxx_arm_cxx24_1:nvcxx_arm_cxx24_3:nvcxx_arm_cxx24_5:nvcxx_arm_cxx24_7:nvcxx_arm_cxx24_9:nvcxx_arm_cxx24_11
+group.nvcxx_arm_cxx.compilers=nvcxx_arm_cxx22_11:nvcxx_arm_cxx23_1:nvcxx_arm_cxx23_3:nvcxx_arm_cxx23_5:nvcxx_arm_cxx23_7:nvcxx_arm_cxx23_9:nvcxx_arm_cxx23_11:nvcxx_arm_cxx24_1:nvcxx_arm_cxx24_3:nvcxx_arm_cxx24_5:nvcxx_arm_cxx24_7:nvcxx_arm_cxx24_9:nvcxx_arm_cxx24_11
 group.nvcxx_arm_cxx.options=
 group.nvcxx_arm_cxx.supportsBinary=true
 group.nvcxx_arm_cxx.binaryHideFuncRe=^(__.*|_(init|start|fini)|(de)?register_tm_clones|call_gmon_start|frame_dummy|\.plt.*|_dl_relocate_static_pie)$
@@ -3738,14 +3747,6 @@ group.nvcxx_arm_cxx.isSemVer=true
 group.nvcxx_arm_cxx.instructionSet=aarch64
 group.nvcxx_arm_cxx.compilerCategories=nvc++
 
-compiler.nvcxx_arm_cxx22_7.demangler=/opt/compiler-explorer/hpc_sdk/Linux_aarch64/22.7/compilers/bin/nvdecode
-compiler.nvcxx_arm_cxx22_7.exe=/opt/compiler-explorer/hpc_sdk/Linux_aarch64/22.7/compilers/bin/nvc++
-compiler.nvcxx_arm_cxx22_7.semver=22.7
-
-compiler.nvcxx_arm_cxx22_9.demangler=/opt/compiler-explorer/hpc_sdk/Linux_aarch64/22.9/compilers/bin/nvdecode
-compiler.nvcxx_arm_cxx22_9.exe=/opt/compiler-explorer/hpc_sdk/Linux_aarch64/22.9/compilers/bin/nvc++
-compiler.nvcxx_arm_cxx22_9.semver=22.9
-
 compiler.nvcxx_arm_cxx22_11.demangler=/opt/compiler-explorer/hpc_sdk/Linux_aarch64/22.11/compilers/bin/nvdecode
 compiler.nvcxx_arm_cxx22_11.exe=/opt/compiler-explorer/hpc_sdk/Linux_aarch64/22.11/compilers/bin/nvc++
 compiler.nvcxx_arm_cxx22_11.semver=22.11
@@ -3844,7 +3845,7 @@ compiler.vast-trunk.options=-resource-dir /opt/compiler-explorer/clang-17.0.1/li
 #################################
 #################################
 # Installed libs
-libs=abseil:array:async_simple:belleviews:beman_iterator_interface:beman_optional26:benchmark:benri:blaze:boost:bmulti:brigand:catch2:cccl:cctz:cereal:cmcstl2:cnl:cppcoro:cppitertools:cpptrace:crosscables:ctbignum:cthash:ctre:date:dataframe:dawjson:dlib:doctest:eastl:eigen:enoki:entt:etl:eve:expected_lite:fastor:flux:fmt:gcem:gemmlowp:glaze:glm:gnufs:gnulibbacktrace:gnuexp:googletest:gsl:hdf5:hedley:hfsm:highfive:highway:hotels-template-library:immer:jsoncons:jsoncpp:kiwaku:kokkos:kumi:kvasir:kyosu:lager:lagom:lexy:libassert:libbpf:libguarded:libsimdpp:libuv:llvm:llvmfs:lua:magic_enum:mfem:mimicpp:mlir:mp-coro:mp-units:namedtype:nanorange:nlohmann_json:nsimd:ofw:openssl:outcome:pegtl:pipes:pugixml:pybind11:python:rangesv3:raberu:reactive_plus_plus:scnlib:seastar:seqan3:simde:simdjson:sol2:spdlog:spy:stdexec:strong_type:taojson:taskflow:tbb:thinkcell:tlexpected:toml11:tomlplusplus:trompeloeil:tts:type_safe:unifex:ureact:vcl:xercesc:xsimd:xtensor:xtl:yomm2:zug:cli11:avr-libstdcpp:curl:copperspice:sqlite:ztdcuneicode:ztdencodingtables:ztdidk:ztdstaticcontainers:ztdtext:ztdplatform:qt:quill:pcre2:widberg-defs:jwt-cpp:xieite
+libs=abseil:array:async_simple:belleviews:beman_iterator_interface:beman_optional26:benchmark:benri:blaze:boost:bmulti:brigand:catch2:cccl:cctz:cereal:cmcstl2:cnl:cppcoro:cppitertools:cpptrace:crosscables:ctbignum:cthash:ctre:date:dataframe:dawjson:dlib:doctest:eastl:eigen:enoki:entt:etl:eve:expected_lite:fastor:flux:fmt:gcem:gemmlowp:glaze:glm:gnufs:gnulibbacktrace:gnuexp:googletest:gsl:hdf5:hedley:hfsm:highfive:highway:hotels-template-library:immer:jsoncons:jsoncpp:kiwaku:kokkos:kumi:kvasir:kyosu:lager:lagom:lexy:libassert:libbpf:libguarded:libsimdpp:libuv:llvm:llvmfs:lua:magic_enum:mfem:mimicpp:mlir:mp-coro:mp-units:namedtype:nanorange:nlohmann_json:nsimd:ofw:openssl:outcome:pegtl:pipes:pugixml:pybind11:python:rangesv3:raberu:reactive_plus_plus:scnlib:seastar:seqan3:simde:simdjson:sol2:spdlog:spy:stdexec:strong_type:taojson:taskflow:tbb:thinkcell:tlexpected:toml11:tomlplusplus:trompeloeil:tts:type_safe:unifex:ureact:vcl:xercesc:xsimd:xtensor:xtl:yomm2:zug:cli11:avr-libstdcpp:curl:copperspice:sqlite:ztdcuneicode:ztdencodingtables:ztdidk:ztdstaticcontainers:ztdtext:ztdplatform:qt:quill:pcre2:widberg-defs:jwt-cpp:xieite:option
 
 
 libs.abseil.name=Abseil
@@ -5678,6 +5679,13 @@ libs.xieite.versions=header
 libs.xieite.versions.header.version=header-only
 libs.xieite.versions.header.path=/opt/compiler-explorer/libs/xieite/header/include
 
+libs.option.name=option
+libs.option.description=Replacement for std::optional with efficient memory usage and additional features
+libs.option.versions=131
+libs.option.url=https://github.com/NUCLEAR-BOMB/option
+libs.option.versions.131.version=1.3.1
+libs.option.versions.131.path=/opt/compiler-explorer/libs/option/v1.3.1/include
+
 #################################
 #################################
 # Installed tools
diff --git a/etc/config/c++.amazonwin.properties b/etc/config/c++.amazonwin.properties
index d41bb5f6a..589664554 100644
--- a/etc/config/c++.amazonwin.properties
+++ b/etc/config/c++.amazonwin.properties
@@ -69,7 +69,7 @@ compiler.mingw64_ucrt_clang_1403.exe=Z:/compilers/mingw-w64-11.3.0-14.0.3-10.0.0
 compiler.mingw64_ucrt_clang_1403.semver=14.0.3
 
 group.vcpp.compilers=&vcpp_x86:&vcpp_x64:&vcpp_arm64
-group.vcpp.options=-EHsc
+group.vcpp.options=/EHsc /source-charset:utf-8
 group.vcpp.compilerType=win32-vc
 group.vcpp.needsMulti=false
 group.vcpp.includeFlag=/I
@@ -110,36 +110,42 @@ compiler.vcpp_v19_20_VS16_0_x86.libPath=Z:/compilers/msvc/14.20.27508-14.20.2752
 compiler.vcpp_v19_20_VS16_0_x86.includePath=Z:/compilers/msvc/14.20.27508-14.20.27525.0/include;Z:/compilers/windows-kits-10/include/10.0.22621.0/cppwinrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/shared;Z:/compilers/windows-kits-10/include/10.0.22621.0/ucrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/um;Z:/compilers/windows-kits-10/include/10.0.22621.0/winrt;
 compiler.vcpp_v19_20_VS16_0_x86.name=x86 msvc v19.20 VS16.0
 compiler.vcpp_v19_20_VS16_0_x86.semver=14.20.27525.0
+compiler.vcpp_v19_20_VS16_0_x86.alias=vcpp_v19_20_x86
 
 compiler.vcpp_v19_20_VS16_0_x64.exe=Z:/compilers/msvc/14.20.27508-14.20.27525.0/bin/Hostx64/x64/cl.exe
 compiler.vcpp_v19_20_VS16_0_x64.libPath=Z:/compilers/msvc/14.20.27508-14.20.27525.0/lib;Z:/compilers/msvc/14.20.27508-14.20.27525.0/lib/x64;Z:/compilers/msvc/14.20.27508-14.20.27525.0/atlmfc/lib/x64;Z:/compilers/msvc/14.20.27508-14.20.27525.0/ifc/x64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/ucrt/x64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/um/x64;
 compiler.vcpp_v19_20_VS16_0_x64.includePath=Z:/compilers/msvc/14.20.27508-14.20.27525.0/include;Z:/compilers/windows-kits-10/include/10.0.22621.0/cppwinrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/shared;Z:/compilers/windows-kits-10/include/10.0.22621.0/ucrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/um;Z:/compilers/windows-kits-10/include/10.0.22621.0/winrt;
 compiler.vcpp_v19_20_VS16_0_x64.name=x64 msvc v19.20 VS16.0
 compiler.vcpp_v19_20_VS16_0_x64.semver=14.20.27525.0
+compiler.vcpp_v19_20_VS16_0_x64.alias=vcpp_v19_20_x64
 
 compiler.vcpp_v19_20_VS16_0_arm64.exe=Z:/compilers/msvc/14.20.27508-14.20.27525.0/bin/Hostx64/arm64/cl.exe
 compiler.vcpp_v19_20_VS16_0_arm64.libPath=Z:/compilers/msvc/14.20.27508-14.20.27525.0/lib;Z:/compilers/msvc/14.20.27508-14.20.27525.0/lib/arm64;Z:/compilers/msvc/14.20.27508-14.20.27525.0/atlmfc/lib/arm64;Z:/compilers/msvc/14.20.27508-14.20.27525.0/ifc/arm64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/ucrt/arm64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/um/arm64;
 compiler.vcpp_v19_20_VS16_0_arm64.includePath=Z:/compilers/msvc/14.20.27508-14.20.27525.0/include;Z:/compilers/windows-kits-10/include/10.0.22621.0/cppwinrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/shared;Z:/compilers/windows-kits-10/include/10.0.22621.0/ucrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/um;Z:/compilers/windows-kits-10/include/10.0.22621.0/winrt;
 compiler.vcpp_v19_20_VS16_0_arm64.name=arm64 msvc v19.20 VS16.0
 compiler.vcpp_v19_20_VS16_0_arm64.semver=14.20.27525.0
+compiler.vcpp_v19_20_VS16_0_arm64.alias=vcpp_v19_20_arm64
 
 compiler.vcpp_v19_21_VS16_1_x86.exe=Z:/compilers/msvc/14.21.27702-14.21.27702.2/bin/Hostx64/x86/cl.exe
 compiler.vcpp_v19_21_VS16_1_x86.libPath=Z:/compilers/msvc/14.21.27702-14.21.27702.2/lib;Z:/compilers/msvc/14.21.27702-14.21.27702.2/lib/x86;Z:/compilers/msvc/14.21.27702-14.21.27702.2/atlmfc/lib/x86;Z:/compilers/msvc/14.21.27702-14.21.27702.2/ifc/x86;Z:/compilers/windows-kits-10/lib/10.0.22621.0/ucrt/x86;Z:/compilers/windows-kits-10/lib/10.0.22621.0/um/x86;
 compiler.vcpp_v19_21_VS16_1_x86.includePath=Z:/compilers/msvc/14.21.27702-14.21.27702.2/include;Z:/compilers/windows-kits-10/include/10.0.22621.0/cppwinrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/shared;Z:/compilers/windows-kits-10/include/10.0.22621.0/ucrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/um;Z:/compilers/windows-kits-10/include/10.0.22621.0/winrt;
 compiler.vcpp_v19_21_VS16_1_x86.name=x86 msvc v19.21 VS16.1
 compiler.vcpp_v19_21_VS16_1_x86.semver=14.21.27702.2
+compiler.vcpp_v19_21_VS16_1_x86.alias=vcpp_v19_21_x86
 
 compiler.vcpp_v19_21_VS16_1_x64.exe=Z:/compilers/msvc/14.21.27702-14.21.27702.2/bin/Hostx64/x64/cl.exe
 compiler.vcpp_v19_21_VS16_1_x64.libPath=Z:/compilers/msvc/14.21.27702-14.21.27702.2/lib;Z:/compilers/msvc/14.21.27702-14.21.27702.2/lib/x64;Z:/compilers/msvc/14.21.27702-14.21.27702.2/atlmfc/lib/x64;Z:/compilers/msvc/14.21.27702-14.21.27702.2/ifc/x64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/ucrt/x64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/um/x64;
 compiler.vcpp_v19_21_VS16_1_x64.includePath=Z:/compilers/msvc/14.21.27702-14.21.27702.2/include;Z:/compilers/windows-kits-10/include/10.0.22621.0/cppwinrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/shared;Z:/compilers/windows-kits-10/include/10.0.22621.0/ucrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/um;Z:/compilers/windows-kits-10/include/10.0.22621.0/winrt;
 compiler.vcpp_v19_21_VS16_1_x64.name=x64 msvc v19.21 VS16.1
 compiler.vcpp_v19_21_VS16_1_x64.semver=14.21.27702.2
+compiler.vcpp_v19_21_VS16_1_x64.alias=vcpp_v19_21_x64
 
 compiler.vcpp_v19_21_VS16_1_arm64.exe=Z:/compilers/msvc/14.21.27702-14.21.27702.2/bin/Hostx64/arm64/cl.exe
 compiler.vcpp_v19_21_VS16_1_arm64.libPath=Z:/compilers/msvc/14.21.27702-14.21.27702.2/lib;Z:/compilers/msvc/14.21.27702-14.21.27702.2/lib/arm64;Z:/compilers/msvc/14.21.27702-14.21.27702.2/atlmfc/lib/arm64;Z:/compilers/msvc/14.21.27702-14.21.27702.2/ifc/arm64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/ucrt/arm64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/um/arm64;
 compiler.vcpp_v19_21_VS16_1_arm64.includePath=Z:/compilers/msvc/14.21.27702-14.21.27702.2/include;Z:/compilers/windows-kits-10/include/10.0.22621.0/cppwinrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/shared;Z:/compilers/windows-kits-10/include/10.0.22621.0/ucrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/um;Z:/compilers/windows-kits-10/include/10.0.22621.0/winrt;
 compiler.vcpp_v19_21_VS16_1_arm64.name=arm64 msvc v19.21 VS16.1
 compiler.vcpp_v19_21_VS16_1_arm64.semver=14.21.27702.2
+compiler.vcpp_v19_21_VS16_1_arm64.alias=vcpp_v19_21_arm64
 
 compiler.vcpp_v19_22_VS16_2_x86.exe=Z:/compilers/msvc/14.22.27905-14.22.27905.0/bin/Hostx64/x86/cl.exe
 compiler.vcpp_v19_22_VS16_2_x86.libPath=Z:/compilers/msvc/14.22.27905-14.22.27905.0/lib;Z:/compilers/msvc/14.22.27905-14.22.27905.0/lib/x86;Z:/compilers/msvc/14.22.27905-14.22.27905.0/atlmfc/lib/x86;Z:/compilers/msvc/14.22.27905-14.22.27905.0/ifc/x86;Z:/compilers/windows-kits-10/lib/10.0.22621.0/ucrt/x86;Z:/compilers/windows-kits-10/lib/10.0.22621.0/um/x86;
@@ -152,6 +158,7 @@ compiler.vcpp_v19_22_VS16_2_x64.libPath=Z:/compilers/msvc/14.22.27905-14.22.2790
 compiler.vcpp_v19_22_VS16_2_x64.includePath=Z:/compilers/msvc/14.22.27905-14.22.27905.0/include;Z:/compilers/windows-kits-10/include/10.0.22621.0/cppwinrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/shared;Z:/compilers/windows-kits-10/include/10.0.22621.0/ucrt;Z:/compilers/windows-kits-10/include/10.0.22621.0/um;Z:/compilers/windows-kits-10/include/10.0.22621.0/winrt;
 compiler.vcpp_v19_22_VS16_2_x64.name=x64 msvc v19.22 VS16.2
 compiler.vcpp_v19_22_VS16_2_x64.semver=14.22.27905.0
+compiler.vcpp_v19_22_VS16_2_x64.alias=vcpp_v19_22_x64
 
 compiler.vcpp_v19_22_VS16_2_arm64.exe=Z:/compilers/msvc/14.22.27905-14.22.27905.0/bin/Hostx64/arm64/cl.exe
 compiler.vcpp_v19_22_VS16_2_arm64.libPath=Z:/compilers/msvc/14.22.27905-14.22.27905.0/lib;Z:/compilers/msvc/14.22.27905-14.22.27905.0/lib/arm64;Z:/compilers/msvc/14.22.27905-14.22.27905.0/atlmfc/lib/arm64;Z:/compilers/msvc/14.22.27905-14.22.27905.0/ifc/arm64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/ucrt/arm64;Z:/compilers/windows-kits-10/lib/10.0.22621.0/um/arm64;
diff --git a/etc/config/c.amazonwin.properties b/etc/config/c.amazonwin.properties
index 1fa5a194d..f5b1f5da8 100644
--- a/etc/config/c.amazonwin.properties
+++ b/etc/config/c.amazonwin.properties
@@ -50,7 +50,7 @@ compiler.cmingw64_ucrt_clang_1403.exe=Z:/compilers/mingw-w64-11.3.0-14.0.3-10.0.
 compiler.cmingw64_ucrt_clang_1403.semver=14.0.3
 
 group.vc.compilers=&vc_x86:&vc_x64:&vc_arm64
-group.vc.options=-EHsc
+group.vc.options=/EHsc /source-charset:utf-8
 group.vc.compilerType=win32-vc
 group.vc.needsMulti=false
 group.vc.includeFlag=/I
diff --git a/etc/config/compiler-explorer.defaults.properties b/etc/config/compiler-explorer.defaults.properties
index 339c22dab..b50a5be42 100644
--- a/etc/config/compiler-explorer.defaults.properties
+++ b/etc/config/compiler-explorer.defaults.properties
@@ -1,6 +1,6 @@
 # Default settings for GCC Explorer.
-compileTimeoutMs=20000
-binaryExecTimeoutMs=10000
+compileTimeoutMs=200000
+binaryExecTimeoutMs=100000
 defaultSource=builtin
 cacheConfig=InMemory(50)
 executableCacheConfig=InMemory(50)
@@ -43,7 +43,7 @@ remoteStorageServer=https://godbolt.org
 # this timeout exists only to work around a bug (#1875)
 # it is set to 5 minutes so that it will never be triggered
 # under any normal circumstances
-compilationEnvTimeoutMs=300000
+compilationEnvTimeoutMs=3000000
 # this timeout determines when a compilation in the queue is abandoned because it has been waiting too long and the
 # client will have given up on it by now
 compilationStaleAfterMs=100000
diff --git a/etc/config/cuda.amazon.properties b/etc/config/cuda.amazon.properties
index 04e145ec9..6d956f5b2 100644
--- a/etc/config/cuda.amazon.properties
+++ b/etc/config/cuda.amazon.properties
@@ -392,7 +392,7 @@ compiler.hipclang-staging-rocm-60102.nvdisasm=/opt/compiler-explorer/clang-rocm-
 compiler.hipclang-staging-rocm-60102.objdumper=/opt/compiler-explorer/clang-rocm-trunk/bin/llvm-objdump
 compiler.hipclang-staging-rocm-60102.options=-x hip --cuda-device-only -O3 -D__HIP_PLATFORM_AMD__=1 -D__HIP_PLATFORM_HCC__=1 -isystem /opt/compiler-explorer/libs/rocm/6.1.2/include --rocm-path=/opt/compiler-explorer/libs/rocm/6.1.2 -include hip/hip_runtime.h
 
-group.nvrtc.compilers=nvrtc121:nvrtc120u1:nvrtc120:nvrtc118:nvrtc117u1:nvrtc117:nvrtc116u2:nvrtc116u1:nvrtc116:nvrtc115u2:nvrtc115u1:nvrtc115:nvrtc114u1:nvrtc114:nvrtc113u1:nvrtc113:nvrtc112u2:nvrtc112u1:nvrtc112:nvrtc111u1:nvrtc111:nvrtc11u1:nvrtc11:nvrtc102:nvrtc101u2:nvrtc101u1:nvrtc101:nvrtc100:nvrtc92:nvrtc91
+group.nvrtc.compilers=nvrtc121:nvrtc120u1:nvrtc120:nvrtc118:nvrtc117u1:nvrtc117:nvrtc116u2:nvrtc116u1:nvrtc116:nvrtc115u2:nvrtc115u1:nvrtc115:nvrtc114u1:nvrtc114:nvrtc113u1:nvrtc113:nvrtc112u2:nvrtc112u1:nvrtc112:nvrtc111u1:nvrtc111:nvrtc11u1:nvrtc11
 group.nvrtc.compilerType=nvrtc
 group.nvrtc.isSemVer=true
 group.nvrtc.baseName=NVRTC
@@ -402,20 +402,6 @@ group.nvrtc.objdumper=/opt/compiler-explorer/cuda/12.1.0/bin/nvdisasm
 group.nvrtc.rpathFlag=-L # WAR, really need `-Xcompiler "-Wl,-rpath=<path>"`, but not required because supportsExecute=false
 group.nvrtc.options=
 group.nvrtc.demangler=/opt/compiler-explorer/cuda/12.0.1/bin/cu++filt
-compiler.nvrtc91.semver=9.1.85
-compiler.nvrtc91.exe=/opt/compiler-explorer/cuda/9.1.85/bin/nvrtc_cli
-compiler.nvrtc92.semver=9.2.88
-compiler.nvrtc92.exe=/opt/compiler-explorer/cuda/9.2.88/bin/nvrtc_cli
-compiler.nvrtc100.semver=10.0.130
-compiler.nvrtc100.exe=/opt/compiler-explorer/cuda/10.0.130/bin/nvrtc_cli
-compiler.nvrtc101.semver=10.1.105
-compiler.nvrtc101.exe=/opt/compiler-explorer/cuda/10.1.105/bin/nvrtc_cli
-compiler.nvrtc101u1.semver=10.1.168
-compiler.nvrtc101u1.exe=/opt/compiler-explorer/cuda/10.1.168/bin/nvrtc_cli
-compiler.nvrtc101u2.semver=10.1.243
-compiler.nvrtc101u2.exe=/opt/compiler-explorer/cuda/10.1.243/bin/nvrtc_cli
-compiler.nvrtc102.semver=10.2.89
-compiler.nvrtc102.exe=/opt/compiler-explorer/cuda/10.2.89/bin/nvrtc_cli
 compiler.nvrtc11.semver=11.0.2
 compiler.nvrtc11.exe=/opt/compiler-explorer/cuda/11.0.2/bin/nvrtc_cli
 compiler.nvrtc11u1.semver=11.0.3
diff --git a/etc/config/julia.amazon.properties b/etc/config/julia.amazon.properties
index 784ccfa95..a0fbbcbd5 100644
--- a/etc/config/julia.amazon.properties
+++ b/etc/config/julia.amazon.properties
@@ -1,11 +1,13 @@
 # Default settings for Julia
 compilers=&julia
-defaultCompiler=julia_1_10_0
+defaultCompiler=julia_1_11_2
 compilerType=julia
 
-group.julia.compilers=julia_1_10_0:julia_1_9_3:julia_1_8_5:julia_1_7_3:julia_nightly
+group.julia.compilers=julia_1_11_2:julia_1_10_0:julia_1_9_3:julia_1_8_5:julia_1_7_3:julia_nightly
 group.julia.isSemVer=true
 group.julia.baseName=Julia
+compiler.julia_1_11_2.exe=/opt/compiler-explorer/julia-1.11.2/bin/julia
+compiler.julia_1_11_2.semver=1.11.2
 compiler.julia_1_10_0.exe=/opt/compiler-explorer/julia-1.10.0/bin/julia
 compiler.julia_1_10_0.semver=1.10.0
 compiler.julia_1_9_3.exe=/opt/compiler-explorer/julia-1.9.3/bin/julia
diff --git a/etc/config/slang.amazon.properties b/etc/config/slang.amazon.properties
new file mode 100644
index 000000000..7a25d2d13
--- /dev/null
+++ b/etc/config/slang.amazon.properties
@@ -0,0 +1,13 @@
+compilers=&defaultslangc
+
+defaultCompiler=slangc_2024_14_6
+supportsBinary=false
+compilerType=slang
+instructionSet=spirv
+
+disassemblerPath=/opt/compiler-explorer/SPIRV-Tools-master/build/tools/spirv-dis
+
+group.defaultslangc.compilers=slangc_2024_14_6
+group.defaultslangc.versionFlag=-version
+compiler.slangc_2024_14_6.exe=/opt/compiler-explorer/slang-2024.14.6/bin/slangc
+compiler.slangc_2024_14_6.name=slangc 2024.14.6
diff --git a/etc/config/slang.defaults.properties b/etc/config/slang.defaults.properties
new file mode 100644
index 000000000..6383fa90f
--- /dev/null
+++ b/etc/config/slang.defaults.properties
@@ -0,0 +1,13 @@
+compilers=&defaultslangc
+
+defaultCompiler=slangc_2024_14_6
+supportsBinary=false
+compilerType=slang
+instructionSet=spirv
+
+disassemblerPath=/opt/compiler-explorer/SPIRV-Tools-master/build/tools/spirv-dis
+
+group.defaultslangc.compilers=slangc_2024_14_6
+group.defaultslangc.versionFlag=-version
+compiler.slangc_2024_14_6.exe=/usr/bin/slangc
+compiler.slangc_2024_14_6.name=slangc 2024.14.6
diff --git a/etc/config/solidity.amazon.properties b/etc/config/solidity.amazon.properties
index 692496d7d..4d9e61e93 100644
--- a/etc/config/solidity.amazon.properties
+++ b/etc/config/solidity.amazon.properties
@@ -1,13 +1,10 @@
 compilers=&solc:&zksolc
 defaultCompiler=solc0821
 
-group.solc.compilers=solc036:solc0426:solc0517:solc0612:solc076:solc0821
+group.solc.compilers=solc0426:solc0517:solc0612:solc076:solc0821
 group.solc.compilerType=solidity
 group.solc.supportsBinary=false
 group.solc.instructionSet=evm
-compiler.solc036.exe=/opt/compiler-explorer/solc-0.3.6/solc
-compiler.solc036.semver=0.3.6
-compiler.solc036.name=solc 0.3.6
 compiler.solc0426.exe=/opt/compiler-explorer/solc-0.4.26/solc
 compiler.solc0426.semver=0.4.26
 compiler.solc0426.name=solc 0.4.26
diff --git a/etc/config/sponsors.yaml b/etc/config/sponsors.yaml
index 50515a3ea..7f0c6242b 100644
--- a/etc/config/sponsors.yaml
+++ b/etc/config/sponsors.yaml
@@ -472,3 +472,18 @@ levels:
       - Aaron D. Marasco
       - Tomas Karban
       - Abdullah Al-Kheshen
+      - Matthew Kolbe
+      - Cem Dervis
+      - Chris Barlas
+      - Dino Occhialini
+      - Gregory Popovitch
+      - Isaac Cloos
+      - Joachim Meyer
+      - Martin Pernica
+      - Matthew Nielsen
+      - Michael
+      - Mike
+      - Paul Horn
+      - Per Tillisch
+      - Tobias Neukom
+      - mason
diff --git a/examples/slang/default.slang b/examples/slang/default.slang
new file mode 100644
index 000000000..640ac6b5a
--- /dev/null
+++ b/examples/slang/default.slang
@@ -0,0 +1,12 @@
+// Default target is 'spirv' but need to add entry for other targets
+StructuredBuffer<float> buffer0;
+StructuredBuffer<float> buffer1;
+RWStructuredBuffer<float> result;
+
+[shader("compute")]
+[numthreads(1,1,1)]
+void computeMain(uint3 threadId : SV_DispatchThreadID)
+{
+    uint index = threadId.x;
+    result[index] = buffer0[index] + buffer1[index];
+}
\ No newline at end of file
diff --git a/lib/base-compiler.ts b/lib/base-compiler.ts
index 1d492d84e..7c9623169 100644
--- a/lib/base-compiler.ts
+++ b/lib/base-compiler.ts
@@ -1330,9 +1330,13 @@ export class BaseCompiler {
     }
 
     async applyClangFormat(output: string): Promise<string> {
-        // Currently hard-coding llvm style
         try {
-            const [stdout, stderr] = await this.env.formatHandler.internalFormat('clangformat', 'LLVM', output);
+            // Currently hard-coding llvm style
+            const {stdout, stderr} = await this.env.formattingService.format('clangformat', output, {
+                baseStyle: 'LLVM',
+                tabWidth: 4,
+                useSpaces: true,
+            });
             if (stderr) {
                 return stdout + '\n/* clang-format stderr:\n' + stderr.trim() + '\n*/';
             }
diff --git a/lib/compilation-env.ts b/lib/compilation-env.ts
index 4fc8ba08b..30d660b7a 100644
--- a/lib/compilation-env.ts
+++ b/lib/compilation-env.ts
@@ -35,7 +35,7 @@ import type {Cache} from './cache/base.interfaces.js';
 import {BaseCache} from './cache/base.js';
 import {createCacheFromConfig} from './cache/from-config.js';
 import {CompilationQueue, EnqueueOptions, Job} from './compilation-queue.js';
-import {FormattingHandler} from './handlers/formatting.js';
+import {FormattingService} from './formatting-service.js';
 import {logger} from './logger.js';
 import type {PropertyGetter} from './properties.interfaces.js';
 import {CompilerProps, PropFunc} from './properties.js';
@@ -54,7 +54,6 @@ export class CompilationEnvironment {
     reportCacheEvery: number;
     multiarch: string | null;
     baseEnv: Record<string, string>;
-    formatHandler: FormattingHandler;
     possibleToolchains?: CompilerOverrideOptions;
     statsNoter: IStatsNoter;
     private logCompilerCacheAccesses: boolean;
@@ -63,6 +62,7 @@ export class CompilationEnvironment {
         compilerProps: CompilerProps,
         awsProps: PropFunc,
         compilationQueue: CompilationQueue | undefined,
+        public formattingService: FormattingService,
         doCache?: boolean,
     ) {
         this.ceProps = compilerProps.ceProps;
@@ -106,9 +106,6 @@ export class CompilationEnvironment {
             if (environmentVariable === '') return;
             this.baseEnv[environmentVariable] = process.env[environmentVariable] ?? '';
         });
-        // I'm not sure that this is the best design; but each compiler having its own means each constructs its own
-        // handler, and passing it in from the outside is a pain as each compiler's constructor needs it.
-        this.formatHandler = new FormattingHandler(this.ceProps);
         this.logCompilerCacheAccesses = this.ceProps('logCompilerCacheAccesses', false);
         this.statsNoter = createStatsNoter(this.ceProps);
     }
diff --git a/lib/compilers/_all.ts b/lib/compilers/_all.ts
index ba167e568..51d77c30d 100644
--- a/lib/compilers/_all.ts
+++ b/lib/compilers/_all.ts
@@ -124,6 +124,7 @@ export {RustcCgGCCCompiler} from './rustc-cg-gcc.js';
 export {RustCompiler} from './rust.js';
 export {ScalaCompiler} from './scala.js';
 export {SdccCompiler} from './sdcc.js';
+export {SlangCompiler} from './slang.js';
 export {SolidityCompiler} from './solidity.js';
 export {SolidityZKsyncCompiler} from './solidity-zksync.js';
 export {SpiceCompiler} from './spice.js';
diff --git a/lib/compilers/dotnet.ts b/lib/compilers/dotnet.ts
index 926911b40..5b76a9a95 100644
--- a/lib/compilers/dotnet.ts
+++ b/lib/compilers/dotnet.ts
@@ -540,7 +540,7 @@ do()
         const isIlDasm = this.compiler.group === 'dotnetildasm';
         const isIlSpy = this.compiler.group === 'dotnetilspy';
         const isCoreRun = this.compiler.group === 'dotnetcoreclr';
-        const toolOptions: string[] = isIlDasm || isIlSpy || isCoreRun ? [] : ['--parallelism', '1'];
+        const toolOptions: string[] = [];
 
         let overrideDiffable = false;
         let overrideDisasm = false;
@@ -559,7 +559,7 @@ do()
                 continue;
             }
 
-            if (isCoreRun && (currentOption === '-e' || currentOption === '--env')) {
+            if ((isCoreRun || isMono) && (currentOption === '-e' || currentOption === '--env')) {
                 const envVar = options.shift();
                 if (envVar) {
                     const [name] = envVar.split('=');
@@ -617,16 +617,22 @@ do()
             }
         }
 
+        const needCodegenOptions = isCrossgen2 || isAot;
+
+        if (needCodegenOptions) {
+            toolOptions.push('--parallelism', '1');
+        }
+
         if (!isIlDasm && !isIlSpy) {
             if (!overrideDiffable && compilerInfo.sdkMajorVersion < 8) {
-                if (!isCoreRun) {
+                if (needCodegenOptions) {
                     toolOptions.push('--codegenopt', 'JitDiffableDasm=1');
                 }
                 envVarFileContents.push('DOTNET_JitDiffableDasm=1');
             }
 
             if (!overrideDisasm) {
-                if (!isCoreRun) {
+                if (needCodegenOptions) {
                     toolOptions.push(
                         '--codegenopt',
                         compilerInfo.sdkMajorVersion === 6 ? 'NgenDisasm=*' : 'JitDisasm=*',
@@ -636,7 +642,7 @@ do()
             }
 
             if (!overrideAssembly) {
-                if (!isCoreRun && compilerInfo.sdkMajorVersion >= 9) {
+                if (needCodegenOptions && compilerInfo.sdkMajorVersion >= 9) {
                     toolOptions.push('--codegenopt', 'JitDisasmAssemblies=CompilerExplorer');
                 }
                 envVarFileContents.push('DOTNET_JitDisasmAssemblies=CompilerExplorer');
@@ -810,7 +816,7 @@ do()
         const ilspyPath = path.join(ilspyToolsDir, targetFramework, 'any', 'ilspycmd.dll');
 
         // prettier-ignore
-        const ilspyOptions = [ilspyPath, dllPath, '--disable-updatecheck'].concat(options);
+        const ilspyOptions = [ilspyPath, dllPath, '--disable-updatecheck', '-r', this.clrBuildDir].concat(options);
         const compilerPath = useDotNetHost ? this.compiler.exe : this.corerunPath;
         const compilerExecResult = await this.exec(compilerPath, ilspyOptions, execOptions);
         const result = this.transformToCompilationResult(compilerExecResult, dllPath);
diff --git a/lib/compilers/java.ts b/lib/compilers/java.ts
index 5501b9e1c..823018681 100644
--- a/lib/compilers/java.ts
+++ b/lib/compilers/java.ts
@@ -26,12 +26,19 @@ import path from 'path';
 
 import fs from 'fs-extra';
 import Semver from 'semver';
+import _ from 'underscore';
 
 import type {ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
-import {BypassCache, CacheKey, CompilationResult} from '../../types/compilation/compilation.interfaces.js';
+import {
+    BypassCache,
+    CacheKey,
+    CompilationResult,
+    ExecutionOptionsWithEnv,
+} from '../../types/compilation/compilation.interfaces.js';
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
 import {ExecutableExecutionOptions} from '../../types/execution/execution.interfaces.js';
 import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
+import type {SelectedLibraryVersion} from '../../types/libraries/libraries.interfaces.js';
 import {assert, unwrap} from '../assert.js';
 import {BaseCompiler, SimpleOutputFilenameCompiler} from '../base-compiler.js';
 import {CompilationEnvironment} from '../compilation-env.js';
@@ -48,6 +55,8 @@ export class JavaCompiler extends BaseCompiler implements SimpleOutputFilenameCo
     javaRuntime: string;
     mainRegex: RegExp;
 
+    libPaths: string[];
+
     constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) {
         super(
             {
@@ -59,12 +68,42 @@ export class JavaCompiler extends BaseCompiler implements SimpleOutputFilenameCo
         );
         this.javaRuntime = this.compilerProps<string>(`compiler.${this.compiler.id}.runtime`);
         this.mainRegex = /public static ?(.*?) void main\(java\.lang\.String\[]\)/;
+        this.libPaths = [];
     }
 
     override getSharedLibraryPathsAsArguments() {
         return [];
     }
 
+    override async runCompiler(
+        compiler: string,
+        options: string[],
+        inputFilename: string,
+        execOptions: ExecutionOptionsWithEnv,
+        filters?: ParseFiltersAndOutputOptions,
+    ): Promise<CompilationResult> {
+        if (!execOptions) {
+            execOptions = this.getDefaultExecOptions();
+        }
+
+        if (!execOptions.customCwd) {
+            execOptions.customCwd = path.dirname(inputFilename);
+        }
+
+        // The items in 'options' before the source file are user inputs.
+        const sourceFileOptionIndex = options.findIndex(option => {
+            return option.endsWith('.java');
+        });
+        const userOptions = options.slice(0, sourceFileOptionIndex);
+        const javaOptions = _.compact([...this.getClasspathArgument(), ...userOptions, inputFilename]);
+        const result = await this.exec(compiler, javaOptions, execOptions);
+        return {
+            ...this.transformToCompilationResult(result, inputFilename),
+            languageId: this.getCompilerResultLanguageId(filters),
+            instructionSet: this.getInstructionSetFromCompilerArgs(options),
+        };
+    }
+
     override async objdump(outputFilename: string, result: any, maxSize: number) {
         const dirPath = path.dirname(outputFilename);
         const files = await fs.readdir(dirPath);
@@ -417,4 +456,18 @@ export class JavaCompiler extends BaseCompiler implements SimpleOutputFilenameCo
             textsBeforeMethod,
         };
     }
+
+    getClasspathArgument(): string[] {
+        const libString = this.libPaths.join(':');
+        return libString ? ['-cp', libString] : [''];
+    }
+
+    override getIncludeArguments(libraries: SelectedLibraryVersion[], dirPath: string): string[] {
+        this.libPaths = libraries.flatMap(selectedLib => {
+            const foundVersion = this.findLibVersion(selectedLib);
+            if (!foundVersion) return [];
+            return foundVersion.path;
+        });
+        return this.libPaths;
+    }
 }
diff --git a/lib/compilers/rust.ts b/lib/compilers/rust.ts
index 253880b74..0d70412c1 100644
--- a/lib/compilers/rust.ts
+++ b/lib/compilers/rust.ts
@@ -94,7 +94,12 @@ export class RustCompiler extends BaseCompiler {
                     ? this.getIrOutputFilename(inputFilename, filters)
                     : opt,
             );
-
+        // Rust intermediate files that get copied to the output get a name of the form "base-HASH-*" -- so
+        // if there are any other compiles running on the same code with `--emit-llvm` then those will clash
+        // with the output and lead to corruptions/missing files. We use a uniquifier here for each potentially
+        // concurrent pass. With thanks to @bjorn3.
+        // See https://github.com/compiler-explorer/compiler-explorer/issues/7012 for example
+        newOptions.push('--codegen', 'extra-filename=llvm-ir-gen');
         return await super.generateIR(inputFilename, newOptions, irOptions, produceCfg, filters);
     }
 
diff --git a/lib/compilers/slang.ts b/lib/compilers/slang.ts
new file mode 100644
index 000000000..5285590b1
--- /dev/null
+++ b/lib/compilers/slang.ts
@@ -0,0 +1,129 @@
+// Copyright (c) 2024, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+import path from 'path';
+
+import type {ExecutionOptions} from '../../types/compilation/compilation.interfaces.js';
+import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
+import {BaseCompiler} from '../base-compiler.js';
+import {logger} from '../logger.js';
+import {SPIRVAsmParser} from '../parsers/asm-parser-spirv.js';
+import * as utils from '../utils.js';
+
+export class SlangCompiler extends BaseCompiler {
+    protected disassemblerPath: string;
+    protected spirvAsm: SPIRVAsmParser;
+
+    static get key() {
+        return 'slang';
+    }
+
+    constructor(info: any, env: any) {
+        super(info, env);
+
+        this.spirvAsm = new SPIRVAsmParser(this.compilerProps);
+
+        this.disassemblerPath = this.compilerProps<string>('disassemblerPath');
+    }
+
+    getTarget(options?: string[]) {
+        if (options) {
+            const index = options.indexOf('-target');
+            if (index !== -1) {
+                return options[index + 1];
+            }
+        }
+        return 'spirv'; // no target found, slang default to 'spirv'
+    }
+
+    getPrimaryOutputFilename(dirPath: string, outputFilebase: string, target: string) {
+        if (target === 'spirv') {
+            return path.join(dirPath, `${outputFilebase}.spv`);
+        } else {
+            // If there is no intermediate file needed, can use file output
+            return this.getOutputFilename(dirPath, outputFilebase);
+        }
+    }
+
+    override optionsForFilter(filters: ParseFiltersAndOutputOptions, outputFilename: string, userOptions?: string[]) {
+        const sourceDir = path.dirname(outputFilename);
+        const target = this.getTarget(userOptions);
+        const spvBinFilename = this.getPrimaryOutputFilename(sourceDir, this.outputFilebase, target);
+        return ['-o', spvBinFilename, '-gminimal']; // -g provides debug info
+    }
+
+    override getOutputFilename(dirPath: string, outputFilebase: string) {
+        return path.join(dirPath, `${outputFilebase}.spvasm`);
+    }
+
+    override async runCompiler(
+        compiler: string,
+        options: string[],
+        inputFilename: string,
+        execOptions: ExecutionOptions & {env: Record<string, string>},
+    ) {
+        const sourceDir = path.dirname(inputFilename);
+        const target = this.getTarget(options);
+        const slangOutputFilename = this.getPrimaryOutputFilename(sourceDir, this.outputFilebase, target);
+
+        if (!execOptions) {
+            execOptions = this.getDefaultExecOptions();
+        }
+        execOptions.customCwd = path.dirname(inputFilename);
+
+        const slangOutput = await this.exec(compiler, options, execOptions);
+        const result = this.transformToCompilationResult(slangOutput, inputFilename);
+
+        if (slangOutput.code !== 0 || !(await utils.fileExists(slangOutputFilename))) {
+            return result;
+        }
+
+        // While slang does have a way to dump spirv assembly, using spirv-dis will make the SPIR-V we
+        // display unified with other things that produce SPIR-V
+        if (target === 'spirv') {
+            const spvasmFilename = this.getOutputFilename(sourceDir, this.outputFilebase);
+            const disassemblerFlags = [slangOutputFilename, '-o', spvasmFilename, '--comment'];
+
+            const spvasmOutput = await this.exec(this.disassemblerPath, disassemblerFlags, execOptions);
+            if (spvasmOutput.code !== 0) {
+                logger.error('SPIR-V binary to text failed', spvasmOutput);
+            }
+
+            result.stdout = result.stdout.concat(utils.parseOutput(spvasmOutput.stdout));
+            result.stderr = result.stderr.concat(utils.parseOutput(spvasmOutput.stderr));
+            result.languageId = 'spirv';
+        }
+
+        return result;
+    }
+
+    // slangc defaults to SPIR-V as a target, but has many target it can output
+    override async processAsm(result, filters: ParseFiltersAndOutputOptions, options: string[]) {
+        if (result.asm.startsWith('; SPIR-V')) {
+            return this.spirvAsm.processAsm(result.asm, filters);
+        }
+        // If not SPIR-V, just display as plain text to be safe
+        return super.processAsm(result, filters, options);
+    }
+}
diff --git a/lib/formatting-service.ts b/lib/formatting-service.ts
new file mode 100644
index 000000000..95ab61493
--- /dev/null
+++ b/lib/formatting-service.ts
@@ -0,0 +1,102 @@
+// Copyright (c) 2024, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+import _ from 'underscore';
+
+import {UnprocessedExecResult} from '../types/execution/execution.interfaces.js';
+
+import * as exec from './exec.js';
+import {FormatOptions} from './formatters/base.interfaces.js';
+import {BaseFormatter, getFormatterTypeByKey} from './formatters/index.js';
+import {logger} from './logger.js';
+import {PropertyGetter} from './properties.interfaces.js';
+
+export class FormattingService {
+    /** Mapping of the formatter ids to the formatter implementations */
+    private registry: Map<string, BaseFormatter> = new Map();
+
+    public getFormatterById(id: string): BaseFormatter | null {
+        return this.registry.get(id) || null;
+    }
+
+    public getFormatters(): BaseFormatter[] {
+        return Array.from(this.registry.values());
+    }
+
+    public async format(formatterid: string, source: string, options: FormatOptions): Promise<UnprocessedExecResult> {
+        const formatter = this.getFormatterById(formatterid);
+        // Ensure the formatter exists
+        if (formatter === null) {
+            throw new Error(`Formatter ${formatterid} not found`);
+        }
+        // Ensure the formatting style is valid
+        if (!formatter.isValidStyle(options.baseStyle)) {
+            throw new Error(`Formatter ${formatterid} does not support style ${options.baseStyle}`);
+        }
+        return await formatter.format(source, options);
+    }
+
+    public async initialize(ceProps: PropertyGetter): Promise<void> {
+        const formatters = _.compact(ceProps('formatters', '').split(':'));
+        for (const formatter of formatters) {
+            logger.info(`Performing discovery of formatter named ${formatter}`);
+            const executable = ceProps<string>(`formatter.${formatter}.exe`);
+            const type = ceProps<string>(`formatter.${formatter}.type`);
+            if (!executable) {
+                logger.warn(`Formatter ${formatter} does not have a valid executable. Skipping...`);
+                continue;
+            }
+            if (!type) {
+                logger.warn(`Formatter ${formatter} does not have a valid type. Skipping...`);
+                continue;
+            }
+            const versionArgument = ceProps<string>(`formatter.${formatter}.version`, '--version');
+            const versionRegExp = ceProps<string>(`formatter.${formatter}.versionRe`, '.*');
+            const hasExplicitVersion = ceProps(`formatter.${formatter}.explicitVersion`, '') !== '';
+            try {
+                const result = await exec.execute(executable, [versionArgument], {});
+                const match = result.stdout.match(versionRegExp);
+                const formatterClass = getFormatterTypeByKey(type);
+                const styleList = ceProps<string>(`formatter.${formatter}.styles`, '');
+                const styles = styleList === '' ? [] : styleList.split(':');
+                // If there is an explicit version, grab it. Otherwise, try to filter the output
+                const version = hasExplicitVersion
+                    ? ceProps<string>(`formatter.${formatter}.explicitVersion`)
+                    : match
+                      ? match[0]
+                      : result.stdout;
+                const instance = new formatterClass({
+                    name: ceProps(`formatter.${formatter}.name`, executable),
+                    exe: executable,
+                    version,
+                    styles,
+                    type,
+                });
+                this.registry.set(formatter, instance);
+            } catch (err: unknown) {
+                logger.warn(`Error while fetching tool info for ${executable}:`, {err});
+            }
+        }
+    }
+}
diff --git a/lib/handlers/api.ts b/lib/handlers/api.ts
index 05880f63d..5835d7eef 100644
--- a/lib/handlers/api.ts
+++ b/lib/handlers/api.ts
@@ -42,7 +42,6 @@ import {BaseShortener, getShortenerTypeByKey} from '../shortener/index.js';
 import {StorageBase} from '../storage/index.js';
 
 import {CompileHandler} from './compile.js';
-import {FormattingHandler} from './formatting.js';
 
 function methodNotAllowed(req: express.Request, res: express.Response) {
     res.send('Method Not Allowed');
@@ -121,19 +120,6 @@ export class ApiHandler {
             .post(compileHandler.handleOptimizationArguments.bind(compileHandler))
             .get(compileHandler.handleOptimizationArguments.bind(compileHandler))
             .all(methodNotAllowed);
-
-        const formatHandler = new FormattingHandler(ceProps);
-        this.handle
-            .route('/format/:tool')
-            .post((req, res) => formatHandler.handle(req, res))
-            .all(methodNotAllowed);
-        this.handle
-            .route('/formats')
-            .get((req, res) => {
-                const all = formatHandler.getFormatterInfo();
-                res.send(all);
-            })
-            .all(methodNotAllowed);
         this.handle.route('/shortlinkinfo/:id').get(this.shortlinkInfoHandler.bind(this)).all(methodNotAllowed);
 
         const shortenerType = getShortenerTypeByKey(urlShortenService);
diff --git a/lib/handlers/api/assembly-documentation-controller.ts b/lib/handlers/api/assembly-documentation-controller.ts
index 9893105f4..85317b6a6 100644
--- a/lib/handlers/api/assembly-documentation-controller.ts
+++ b/lib/handlers/api/assembly-documentation-controller.ts
@@ -24,15 +24,15 @@
 
 import express from 'express';
 
-import {addStaticHeaders} from '../../../app.js';
 import {getDocumentationProviderTypeByKey} from '../../asm-docs/index.js';
+import {cached, cors} from '../middleware.js';
 
 import {HttpController} from './controller.interfaces.js';
 
 export class AssemblyDocumentationController implements HttpController {
     createRouter(): express.Router {
         const router = express.Router();
-        router.get('/api/asm/:arch/:opcode', this.getOpcodeDocumentation.bind(this));
+        router.get('/api/asm/:arch/:opcode', cors, cached, this.getOpcodeDocumentation.bind(this));
         return router;
     }
 
@@ -54,12 +54,10 @@ export class AssemblyDocumentationController implements HttpController {
             const contentType = req.accepts(['text', 'json']);
             switch (contentType) {
                 case 'text': {
-                    addStaticHeaders(res);
                     res.send(information.html);
                     break;
                 }
                 case 'json': {
-                    addStaticHeaders(res);
                     res.send(information);
                     break;
                 }
diff --git a/lib/handlers/api/formatting-controller.ts b/lib/handlers/api/formatting-controller.ts
new file mode 100644
index 000000000..3a348ed8a
--- /dev/null
+++ b/lib/handlers/api/formatting-controller.ts
@@ -0,0 +1,82 @@
+// Copyright (c) 2024, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+import express from 'express';
+
+import {FormattingService} from '../../formatting-service.js';
+import {cached, cors} from '../middleware.js';
+
+import {HttpController} from './controller.interfaces.js';
+
+export class FormattingController implements HttpController {
+    public constructor(private formattingService: FormattingService) {}
+
+    createRouter(): express.Router {
+        const router = express.Router();
+        router.post('/api/format/:tool', cors, cached, this.format.bind(this));
+        router.get('/api/formats', cors, cached, this.getFormatters.bind(this));
+        return router;
+    }
+
+    /** Handle requests to /api/format/:tool */
+    public async format(req: express.Request, res: express.Response) {
+        const formatter = this.formattingService.getFormatterById(req.params.tool);
+        // Ensure the target formatter exists
+        if (formatter === null) {
+            return res.status(422).json({exit: 2, answer: `Unknown format tool '${req.params.tool}'`});
+        }
+        // Ensure there is source code to format
+        if (!req.body || !req.body.source) {
+            res.status(400).json({exit: 0, answer: ''});
+            return;
+        }
+        // Ensure that the requested style is supported by the formatter
+        if (!formatter.isValidStyle(req.body.base)) {
+            return res.status(422).json({exit: 3, answer: `Style '${req.body.base}' is not supported`});
+        }
+        // Do the formatting
+        try {
+            const formatted = await formatter.format(req.body.source, {
+                useSpaces: req.body.useSpaces === undefined ? true : req.body.useSpaces,
+                tabWidth: req.body.tabWidth === undefined ? 4 : req.body.tabWidth,
+                baseStyle: req.body.base,
+            });
+            res.json({exit: formatted.code, answer: formatted.stdout || formatted.stderr || ''});
+        } catch (err: unknown) {
+            res.status(500).json({
+                exit: 1,
+                thrown: true,
+                answer:
+                    (err && Object.hasOwn(err, 'message') && (err as Record<'message', 'string'>).message) ||
+                    'Internal server error',
+            });
+        }
+    }
+
+    /** Handle requests to /api/formats */
+    public async getFormatters(_: express.Request, res: express.Response) {
+        const formatters = this.formattingService.getFormatters();
+        res.send(formatters.map(formatter => formatter.formatterInfo));
+    }
+}
diff --git a/lib/handlers/api/site-template-controller.ts b/lib/handlers/api/site-template-controller.ts
index cc9ccca7b..03333bd86 100644
--- a/lib/handlers/api/site-template-controller.ts
+++ b/lib/handlers/api/site-template-controller.ts
@@ -25,13 +25,14 @@
 import express from 'express';
 
 import {getSiteTemplates} from '../../site-templates.js';
+import {cached, cors} from '../middleware.js';
 
 import {HttpController} from './controller.interfaces.js';
 
 export class SiteTemplateController implements HttpController {
     createRouter(): express.Router {
         const router = express.Router();
-        router.get('/api/siteTemplates', this.getSiteTemplates.bind(this));
+        router.get('/api/siteTemplates', cors, cached, this.getSiteTemplates.bind(this));
         return router;
     }
 
diff --git a/lib/handlers/api/source-controller.ts b/lib/handlers/api/source-controller.ts
index b7a48f609..add1a2111 100644
--- a/lib/handlers/api/source-controller.ts
+++ b/lib/handlers/api/source-controller.ts
@@ -25,8 +25,8 @@
 import express from 'express';
 import _ from 'underscore';
 
-import {addStaticHeaders} from '../../../app.js';
 import {Source} from '../../../types/source.interfaces.js';
+import {cached, cors} from '../middleware.js';
 
 import {HttpController} from './controller.interfaces.js';
 
@@ -35,8 +35,8 @@ export class SourceController implements HttpController {
 
     createRouter(): express.Router {
         const router = express.Router();
-        router.get('/source/:source/list', this.listEntries.bind(this));
-        router.get('/source/:source/load/:language/:filename', this.loadEntry.bind(this));
+        router.get('/source/:source/list', cors, cached, this.listEntries.bind(this));
+        router.get('/source/:source/load/:language/:filename', cors, cached, this.loadEntry.bind(this));
         return router;
     }
 
@@ -50,7 +50,6 @@ export class SourceController implements HttpController {
             return;
         }
         const entries = await source.list();
-        addStaticHeaders(res);
         res.json(entries);
     }
 
@@ -64,7 +63,6 @@ export class SourceController implements HttpController {
             return;
         }
         const entry = await source.load(req.params.language, req.params.filename);
-        addStaticHeaders(res);
         res.json(entry);
     }
 
diff --git a/lib/handlers/formatting.ts b/lib/handlers/formatting.ts
deleted file mode 100644
index 623f04447..000000000
--- a/lib/handlers/formatting.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright (c) 2018, Compiler Explorer Authors
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-//     * Redistributions of source code must retain the above copyright notice,
-//       this list of conditions and the following disclaimer.
-//     * Redistributions in binary form must reproduce the above copyright
-//       notice, this list of conditions and the following disclaimer in the
-//       documentation and/or other materials provided with the distribution.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
-// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-// POSSIBILITY OF SUCH DAMAGE.
-
-import express from 'express';
-import _ from 'underscore';
-
-import * as exec from '../exec.js';
-import {BaseFormatter, getFormatterTypeByKey} from '../formatters/index.js';
-import {logger} from '../logger.js';
-import {PropertyGetter} from '../properties.interfaces.js';
-
-export class FormattingHandler {
-    private formatters: Record<string, BaseFormatter> = {};
-
-    public constructor(private ceProps: PropertyGetter) {
-        const formatters = _.compact(ceProps('formatters', '').split(':'));
-        for (const formatter of formatters) {
-            this.populateFormatterInfo(formatter);
-        }
-    }
-
-    private async populateFormatterInfo(formatterName: string): Promise<void> {
-        const exe = this.ceProps<string>(`formatter.${formatterName}.exe`);
-        const type = this.ceProps<string>(`formatter.${formatterName}.type`);
-        if (!exe) {
-            logger.warn(`Formatter ${formatterName} does not have a valid executable. Skipping...`);
-            return;
-        }
-        if (!type) {
-            logger.warn(`Formatter ${formatterName} does not have a formatter class. Skipping...`);
-            return;
-        }
-        const versionArgument = this.ceProps<string>(`formatter.${formatterName}.version`, '--version');
-        const versionRegExp = this.ceProps<string>(`formatter.${formatterName}.versionRe`, '.*');
-        const hasExplicitVersion = this.ceProps(`formatter.${formatterName}.explicitVersion`, '') !== '';
-        try {
-            const result = await exec.execute(exe, [versionArgument], {});
-            const match = result.stdout.match(versionRegExp);
-            const formatterClass = getFormatterTypeByKey(type);
-            const styleList = this.ceProps<string>(`formatter.${formatterName}.styles`, '');
-            const styles = styleList === '' ? [] : styleList.split(':');
-            // If there is an explicit version, grab it. Otherwise try to filter the output
-            const version = hasExplicitVersion
-                ? this.ceProps<string>(`formatter.${formatterName}.explicitVersion`)
-                : match
-                  ? match[0]
-                  : result.stdout;
-            this.formatters[formatterName] = new formatterClass({
-                name: this.ceProps(`formatter.${formatterName}.name`, exe),
-                exe,
-                version,
-                styles,
-                type,
-            });
-        } catch (err: unknown) {
-            logger.warn(`Error while fetching tool info for ${exe}:`, {err});
-        }
-    }
-
-    public getFormatterInfo() {
-        return Object.values(this.formatters).map(formatter => formatter.formatterInfo);
-    }
-
-    public async handle(req: express.Request, res: express.Response): Promise<void> {
-        const name = req.params.tool;
-        const formatter = this.formatters[name];
-        // Ensure the formatter exists
-        if (!formatter) {
-            res.status(422).send({
-                exit: 2,
-                answer: `Unknown format tool '${name}'`,
-            });
-            return;
-        }
-        // Ensure there is source code to format
-        if (!req.body || !req.body.source) {
-            res.send({exit: 0, answer: ''});
-            return;
-        }
-        // Ensure the wanted style is valid for the formatter
-        const style = req.body.base;
-        if (!formatter.isValidStyle(style)) {
-            res.status(422).send({
-                exit: 3,
-                answer: `Style '${style}' is not supported`,
-            });
-            return;
-        }
-        try {
-            // Perform the actual formatting
-            const result = await formatter.format(req.body.source, {
-                useSpaces: req.body.useSpaces === undefined ? true : req.body.useSpaces,
-                tabWidth: req.body.tabWidth === undefined ? 4 : req.body.tabWidth,
-                baseStyle: req.body.base,
-            });
-            res.send({
-                exit: result.code,
-                answer: result.stdout || result.stderr || '',
-            });
-        } catch (err: unknown) {
-            res.status(500).send({
-                exit: 1,
-                thrown: true,
-                answer:
-                    (err && Object.hasOwn(err, 'message') && (err as Record<'message', 'string'>).message) ||
-                    'Internal server error',
-            });
-        }
-    }
-
-    async internalFormat(formatterName: string, style: string, source: string): Promise<[string, string]> {
-        const formatter = this.formatters[formatterName];
-        // Ensure the formatter exists
-        if (!formatter) {
-            throw new Error('Unknown formatter name');
-        }
-        // Ensure the wanted style is valid for the formatter
-        if (!formatter.isValidStyle(style)) {
-            throw new Error('Unsupported formatter style');
-        }
-        // Perform the actual formatting
-        const result = await formatter.format(source, {
-            useSpaces: true, // hard coded for now, TODO should this be changed?
-            tabWidth: 4,
-            baseStyle: style,
-        });
-        return [result.stdout || '', result.stderr];
-    }
-}
diff --git a/lib/handlers/middleware.ts b/lib/handlers/middleware.ts
new file mode 100644
index 000000000..9b23f2782
--- /dev/null
+++ b/lib/handlers/middleware.ts
@@ -0,0 +1,50 @@
+// Copyright (c) 2024, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+import express from 'express';
+
+import * as props from '../properties.js';
+
+const ceProps = props.propsFor('compiler-explorer');
+const staticMaxAge = ceProps('staticMaxAgeSecs', 31536000);
+
+/** Add static headers to the response */
+export const cached: express.Handler = (_, res, next) => {
+    res.set('Cache-Control', `public, max-age=${staticMaxAge}, must-revalidate`);
+    return next();
+};
+
+/** Allow any client to make cross-origin requests */
+export const cors: express.Handler = (_, res, next) => {
+    res.set('Access-Control-Allow-Origin', '*');
+    res.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
+    return next();
+};
+
+/** Add Content-Security-Policy header to the response */
+export const csp: express.Handler = (_, res, next) => {
+    // TODO: Consider if CSP should be re-enabled
+    // res.set('Content-Security-Policy', `default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-src 'self';`);
+    return next();
+};
diff --git a/lib/handlers/noscript.ts b/lib/handlers/noscript.ts
index 0bf209ad1..94dd8931d 100644
--- a/lib/handlers/noscript.ts
+++ b/lib/handlers/noscript.ts
@@ -35,14 +35,13 @@ import {ClientOptionsHandler} from '../options-handler.js';
 import {StorageBase} from '../storage/index.js';
 
 import {CompileHandler} from './compile.js';
+import {cached, csp} from './middleware.js';
 
 function isMobileViewer(req: express.Request) {
     return req.header('CloudFront-Is-Mobile-Viewer') === 'true';
 }
 
 export class NoScriptHandler {
-    readonly staticHeaders: (res: express.Response) => void;
-    readonly contentPolicyHeader: (res: express.Response) => void;
     readonly clientOptionsHandler: ClientOptionsHandler;
     readonly renderConfig: (a: any, b: any) => any;
     readonly storageHandler: StorageBase;
@@ -56,8 +55,6 @@ export class NoScriptHandler {
         private readonly router: express.Router,
         config: any,
     ) {
-        this.staticHeaders = config.staticHeaders;
-        this.contentPolicyHeader = config.contentPolicyHeader;
         this.clientOptionsHandler = config.clientOptionsHandler;
         this.renderConfig = config.renderConfig;
         this.storageHandler = config.storageHandler;
@@ -74,15 +71,11 @@ export class NoScriptHandler {
         });
 
         this.router
-            .get('/noscript', (req, res) => {
-                this.staticHeaders(res);
-                this.contentPolicyHeader(res);
+            .get('/noscript', cached, csp, (req, res) => {
                 this.renderNoScriptLayout(undefined, req, res);
             })
-            .get('/noscript/z/:id', this.storedStateHandlerNoScript.bind(this))
-            .get('/noscript/sponsors', (req, res) => {
-                this.staticHeaders(res);
-                this.contentPolicyHeader(res);
+            .get('/noscript/z/:id', cached, csp, this.storedStateHandlerNoScript.bind(this))
+            .get('/noscript/sponsors', cached, csp, (req, res) => {
                 res.render(
                     'noscript/sponsors',
                     this.renderConfig(
@@ -94,9 +87,7 @@ export class NoScriptHandler {
                     ),
                 );
             })
-            .get('/noscript/:language', (req, res) => {
-                this.staticHeaders(res);
-                this.contentPolicyHeader(res);
+            .get('/noscript/:language', cached, csp, (req, res) => {
                 this.renderNoScriptLayout(undefined, req, res);
             })
             .post('/api/noscript/compile', this.formDataParser, this.compileHandler.handle.bind(this.compileHandler));
@@ -157,9 +148,6 @@ export class NoScriptHandler {
     }
 
     renderNoScriptLayout(state: ClientState | undefined, req: express.Request, res: express.Response) {
-        this.staticHeaders(res);
-        this.contentPolicyHeader(res);
-
         let wantedLanguage = 'c++';
         if (req.params && req.params.language) {
             wantedLanguage = req.params.language;
diff --git a/lib/handlers/route-api.ts b/lib/handlers/route-api.ts
index 8fbc8fe10..2c0577f4e 100644
--- a/lib/handlers/route-api.ts
+++ b/lib/handlers/route-api.ts
@@ -43,6 +43,7 @@ import * as utils from '../utils.js';
 
 import {ApiHandler} from './api.js';
 import {CompileHandler} from './compile.js';
+import {cached, csp} from './middleware.js';
 
 export type HandlerConfig = {
     compileHandler: CompileHandler;
@@ -53,8 +54,6 @@ export type HandlerConfig = {
     defArgs: AppDefaultArguments;
     renderConfig: any;
     renderGoldenLayout: any;
-    staticHeaders: any;
-    contentPolicyHeader: any;
     compilationEnvironment: CompilationEnvironment;
 };
 
@@ -97,11 +96,11 @@ export class RouteAPI {
     InitializeRoutes() {
         this.router
             .use('/api', this.apiHandler.handle)
-            .get('/z/:id', this.storedStateHandler.bind(this))
-            .get('/z/:id/code/:session', this.storedCodeHandler.bind(this))
-            .get('/resetlayout/:id', this.storedStateHandlerResetLayout.bind(this))
-            .get('/clientstate/:clientstatebase64([^]*)', this.unstoredStateHandler.bind(this))
-            .get('/fromsimplelayout', this.simpleLayoutHandler.bind(this));
+            .get('/z/:id', cached, csp, this.storedStateHandler.bind(this))
+            .get('/z/:id/code/:session', cached, csp, this.storedCodeHandler.bind(this))
+            .get('/resetlayout/:id', cached, csp, this.storedStateHandlerResetLayout.bind(this))
+            .get('/clientstate/:clientstatebase64([^]*)', cached, csp, this.unstoredStateHandler.bind(this))
+            .get('/fromsimplelayout', cached, csp, this.simpleLayoutHandler.bind(this));
     }
 
     storedCodeHandler(req: express.Request, res: express.Response, next: express.NextFunction) {
diff --git a/lib/languages.ts b/lib/languages.ts
index 7bdff9de6..f04cea7c7 100644
--- a/lib/languages.ts
+++ b/lib/languages.ts
@@ -704,6 +704,17 @@ const definitions: Record<LanguageKey, LanguageDefinition> = {
         monacoDisassembly: null,
         digitSeparator: '_',
     },
+    slang: {
+        name: 'Slang',
+        monaco: 'slang',
+        extensions: ['.slang'],
+        alias: [],
+        logoUrl: 'slang.png',
+        logoUrlDark: null,
+        formatter: null,
+        previewFilter: null,
+        monacoDisassembly: null,
+    },
     solidity: {
         name: 'Solidity',
         monaco: 'sol',
diff --git a/lib/parsers/asm-parser-spirv.ts b/lib/parsers/asm-parser-spirv.ts
index e72014bc0..74f0d6183 100644
--- a/lib/parsers/asm-parser-spirv.ts
+++ b/lib/parsers/asm-parser-spirv.ts
@@ -105,6 +105,8 @@ export class SPIRVAsmParser extends AsmParser {
         const opLine = /OpLine/;
         const opNoLine = /OpNoLine/;
         const opExtDbg = /OpExtInst\s+%void\s+%\d+\s+Debug/;
+        const opExtDbgLine = /OpExtInst.*DebugLine %\w+\s+(%\w+)\s+(%\w+)/;
+        const opExtDbgNoLine = /OpExtInst.*DebugNoLine/;
         const opModuleProcessed = /OpModuleProcessed/;
         const opString = /OpString/;
         const opSource = /OpSource/;
@@ -112,6 +114,13 @@ export class SPIRVAsmParser extends AsmParser {
         const opMemberName = /OpMemberName/;
         const opLabel = /OpLabel/;
 
+        // Need to build constants values to get line numbers from DebugInfo.
+        // It is validated these will only be int/uint so can ignore floats
+        const opConstant = /(%\w+)\s+=\s+OpConstant\s+%\w+\s+(\d+)/;
+        const constantIdToValue: Record<string, number> = {};
+        const opDebugSoruce = /(%\w+) = OpExtInst.*DebugSource %\w+\s+(%\w+)/;
+        const idToOpString: Record<string, string> = {};
+
         const labelDef = /^\s*(%\w+)\s*=\s*(?:OpFunction\s+|OpLabel)/;
 
         const unclosedString = /^[^"]+"(?:[^"\\]|\\.)*$/;
@@ -130,7 +139,29 @@ export class SPIRVAsmParser extends AsmParser {
                 continue;
             }
 
-            const match = line.match(sourceTag);
+            let match = line.match(opConstant);
+            if (match) {
+                constantIdToValue[match[1]] = parseInt(match[2]);
+            }
+            match = line.match(opDebugSoruce);
+            if (match) {
+                idToOpString[match[1]] = match[2];
+            }
+
+            // TODO - Currently don't handle if there is 'Line End' being used
+            // (most people just have 'Line Start' and 'Line End' the same)
+            match = line.match(opExtDbgLine);
+            if (match) {
+                const opStringId = idToOpString[match[1]];
+                source = {
+                    file: utils.maskRootdir(opStrings[parseInt(opStringId)]),
+                    line: constantIdToValue[match[1]],
+                    mainsource: true,
+                };
+            }
+
+            // Will only have either OpLine or DebugLine
+            match = line.match(sourceTag);
             if (match) {
                 source = {
                     file: utils.maskRootdir(opStrings[parseInt(match[1])]),
@@ -151,7 +182,7 @@ export class SPIRVAsmParser extends AsmParser {
             }
 
             // OpLabel are by definition the start of a block, so don't include in previous source
-            if (endBlock.test(line) || opNoLine.test(line) || opLabel.test(line)) {
+            if (endBlock.test(line) || opNoLine.test(line) || opExtDbgNoLine.test(line) || opLabel.test(line)) {
                 source = null;
             }
 
diff --git a/lib/utils.ts b/lib/utils.ts
index 48fa31f5f..25b27f489 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -546,3 +546,10 @@ export function getEmptyExecutionResult(): BasicExecutionResult {
 export function deltaTimeNanoToMili(startTime: bigint, endTime: bigint): number {
     return Number((endTime - startTime) / BigInt(1_000_000));
 }
+
+/**
+ * Sleep for a number of milliseconds.
+ */
+export async function sleep(ms: number) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
diff --git a/package-lock.json b/package-lock.json
index d117b0e1c..0f4b46ea3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5989,9 +5989,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001677",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz",
-      "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==",
+      "version": "1.0.30001684",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz",
+      "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -6005,7 +6005,8 @@
           "type": "github",
           "url": "https://github.com/sponsors/ai"
         }
-      ]
+      ],
+      "license": "CC-BY-4.0"
     },
     "node_modules/caseless": {
       "version": "0.12.0",
diff --git a/static/main.ts b/static/main.ts
index 2a688a906..cb30b8a52 100644
--- a/static/main.ts
+++ b/static/main.ts
@@ -644,6 +644,22 @@ function start() {
         sizeCheckNavHideables();
     }
 
+    new clipboard('.btn.clippy');
+
+    const [themer, settings] = setupSettings(hub);
+
+    function handleCtrlS(event: JQuery.KeyDownEvent<Window, undefined, Window, Window>): void {
+        event.preventDefault();
+        if (settings.enableCtrlS === 'false') {
+            // emit short link
+            if (settings.enableSharingPopover) {
+                hub.layout.eventHub.emit('displaySharingPopover');
+            } else {
+                hub.layout.eventHub.emit('copyShortLinkToClip');
+            }
+        }
+    }
+
     $(window)
         .on('resize', sizeRoot)
         .on('beforeunload', () => {
@@ -653,12 +669,13 @@ function start() {
                 if (layout.config.content && layout.config.content.length > 0)
                     sessionThenLocalStorage.set('gl', JSON.stringify(layout.toConfig()));
             }
+        })
+        .on('keydown', event => {
+            if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') {
+                handleCtrlS(event);
+            }
         });
 
-    new clipboard('.btn.clippy');
-
-    const [themer, settings] = setupSettings(hub);
-
     // We assume no consent for embed users
     if (!options.embedded) {
         setupButtons(options, hub);
diff --git a/static/modes/_all.ts b/static/modes/_all.ts
index 2c6171582..26dc48340 100644
--- a/static/modes/_all.ts
+++ b/static/modes/_all.ts
@@ -59,6 +59,7 @@ import './nim-mode';
 import './ocaml-mode';
 import './openclc-mode';
 import './ptx-mode';
+import './slang-mode';
 import './spice-mode';
 import './spirv-mode';
 import './tablegen-mode';
diff --git a/static/modes/slang-mode.ts b/static/modes/slang-mode.ts
new file mode 100644
index 000000000..d210edc95
--- /dev/null
+++ b/static/modes/slang-mode.ts
@@ -0,0 +1,187 @@
+// Copyright (c) 2024, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+import $ from 'jquery';
+
+import * as monaco from 'monaco-editor';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore  "Could not find a declaration file"
+import * as cpp from 'monaco-editor/esm/vs/basic-languages/cpp/cpp';
+
+// Currently just a subset of HLSL
+// Working with Slang dev team to get a unified Monaco set
+// https://github.com/compiler-explorer/compiler-explorer/issues/7180
+function definition(): monaco.languages.IMonarchLanguage {
+    const slang = $.extend(true, {}, cpp.language);
+
+    function addKeywords(keywords: string[]) {
+        for (let i = 0; i < keywords.length; ++i) {
+            slang.keywords.push(keywords[i]);
+        }
+    }
+
+    function vectorMatrixTypes(basename: string) {
+        const types: string[] = [];
+        for (let i = 1; i !== 5; ++i) {
+            for (let j = 1; j !== 5; ++j) {
+                types.push(`${basename}${i}x${j}`);
+            }
+            types.push(`${basename}${i}`);
+        }
+        return types;
+    }
+
+    addKeywords(vectorMatrixTypes('bool'));
+    addKeywords(vectorMatrixTypes('uint'));
+    addKeywords(vectorMatrixTypes('float'));
+    addKeywords(vectorMatrixTypes('int'));
+
+    function resource(name: string) {
+        return [name, `RW${name}`];
+    }
+
+    addKeywords(resource('Buffer'));
+    addKeywords(resource('Texture1D'));
+    addKeywords(resource('Texture1DArray'));
+    addKeywords(resource('Texture2D'));
+    addKeywords(resource('Texture2DArray'));
+    addKeywords(resource('Texture3D'));
+    addKeywords(resource('TextureCube'));
+    addKeywords(resource('TextureCubeArray'));
+    addKeywords(resource('Texture2DMS'));
+    addKeywords(resource('Texture2DMSArray'));
+    addKeywords(resource('ByteAddressBuffer'));
+    addKeywords(resource('StructuredBuffer'));
+    addKeywords(resource('ConstantBuffer'));
+
+    addKeywords([
+        'out',
+        'inout',
+        'vector',
+        'matrix',
+        'uint',
+
+        'SamplerState',
+        'SamplerComparisonState',
+
+        // Additional resource yypes
+        'AppendStructuredBuffer',
+        'ConsumeStructuredBuffer',
+
+        // Intrinsic functions
+        'abort',
+        'abs',
+        'acos',
+        'all',
+        'any',
+        'asin',
+        'asint',
+        'asuint',
+        'atan',
+        'atan2',
+        'ceil',
+        'clamp',
+        'clip',
+        'cos',
+        'cosh',
+        'countbits',
+        'cross',
+        'ddx',
+        'ddx_coarse',
+        'ddx_fine',
+        'ddy',
+        'ddy_coarse',
+        'ddy_fine',
+        'degrees',
+        'determinant',
+        'distance',
+        'dot',
+        'dst',
+        'errorf',
+        'exp',
+        'exp2',
+        'f16tof32',
+        'f32tof16',
+        'faceforward',
+        'firstbithigh',
+        'firstbitlow',
+        'floor',
+        'fma',
+        'fmod',
+        'frac',
+        'frexp',
+        'fwidth',
+        'isfinite',
+        'isinf',
+        'isnan',
+        'ldexp',
+        'length',
+        'lerp',
+        'lit',
+        'log',
+        'log10',
+        'log2',
+        'mad',
+        'max',
+        'min',
+        'modf',
+        'msad4',
+        'mul',
+        'noise',
+        'normalize',
+        'pow',
+        'radians',
+        'rcp',
+        'reflect',
+        'refract',
+        'reversebits',
+        'round',
+        'rsqrt',
+        'saturate',
+        'sign',
+        'sin',
+        'sincos',
+        'sinh',
+        'smoothstep',
+        'sqrt',
+        'step',
+        'tan',
+        'tanh',
+        'transpose',
+        'trunc',
+
+        // Compute + mesh/amplification shaders
+        'numthreads',
+        'outputtopology',
+        'DispatchMesh',
+        'groupshared',
+    ]);
+
+    return slang;
+}
+
+monaco.languages.register({id: 'slang'});
+monaco.languages.setMonarchTokensProvider('slang', definition());
+
+export {};
diff --git a/static/panes/editor.ts b/static/panes/editor.ts
index 6595b81a9..8ee41f745 100644
--- a/static/panes/editor.ts
+++ b/static/panes/editor.ts
@@ -665,8 +665,6 @@ export class Editor extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Edit
                 if (!loadSave.onSaveToFile(this.id.toString())) {
                     this.showLoadSaver();
                 }
-            } else if (this.settings.enableCtrlS === 'false') {
-                this.emitShortLinkEvent();
             } else if (this.settings.enableCtrlS === '2') {
                 this.runFormatDocumentAction();
             } else if (this.settings.enableCtrlS === '3') {
@@ -1056,14 +1054,6 @@ export class Editor extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Edit
         });
     }
 
-    emitShortLinkEvent(): void {
-        if (this.settings.enableSharingPopover) {
-            this.eventHub.emit('displaySharingPopover');
-        } else {
-            this.eventHub.emit('copyShortLinkToClip');
-        }
-    }
-
     runFormatDocumentAction(): void {
         unwrap(this.editor.getAction('editor.action.formatDocument')).run();
     }
diff --git a/types/languages.interfaces.ts b/types/languages.interfaces.ts
index 2ad7b675d..030c197ff 100644
--- a/types/languages.interfaces.ts
+++ b/types/languages.interfaces.ts
@@ -83,6 +83,7 @@ export type LanguageKey =
     | 'ruby'
     | 'rust'
     | 'scala'
+    | 'slang'
     | 'solidity'
     | 'spice'
     | 'spirv'


Test patch:
diff --git a/test/compilation-env.ts b/test/compilation-env.ts
index 786f8ab24..1b97d4f81 100644
--- a/test/compilation-env.ts
+++ b/test/compilation-env.ts
@@ -26,6 +26,7 @@ import './utils.js';
 import {beforeAll, describe, expect, it} from 'vitest';
 
 import {CompilationEnvironment} from '../lib/compilation-env.js';
+import {FormattingService} from '../lib/formatting-service.js';
 import {CompilerProps, fakeProps} from '../lib/properties.js';
 
 const props = {
@@ -43,28 +44,40 @@ describe('Compilation environment', () => {
 
     it('Should cache by default', async () => {
         // TODO: Work will need to be done here when CompilationEnvironment's constructor is typed better
-        const ce = new CompilationEnvironment(compilerProps, fakeProps({}), undefined, undefined);
+        const ce = new CompilationEnvironment(
+            compilerProps,
+            fakeProps({}),
+            undefined,
+            new FormattingService(),
+            undefined,
+        );
         await expect(ce.cacheGet('foo')).resolves.toBeNull();
         await ce.cachePut('foo', {res: 'bar'}, undefined);
         await expect(ce.cacheGet('foo')).resolves.toEqual({res: 'bar'});
         await expect(ce.cacheGet('baz')).resolves.toBeNull();
     });
     it('Should cache when asked', async () => {
-        const ce = new CompilationEnvironment(compilerProps, fakeProps({}), undefined, true);
+        const ce = new CompilationEnvironment(compilerProps, fakeProps({}), undefined, new FormattingService(), true);
         await expect(ce.cacheGet('foo')).resolves.toBeNull();
         await ce.cachePut('foo', {res: 'bar'}, undefined);
         await expect(ce.cacheGet('foo')).resolves.toEqual({res: 'bar'});
     });
     it("Shouldn't cache when asked", async () => {
         // TODO: Work will need to be done here when CompilationEnvironment's constructor is typed better
-        const ce = new CompilationEnvironment(compilerProps, fakeProps({}), undefined, false);
+        const ce = new CompilationEnvironment(compilerProps, fakeProps({}), undefined, new FormattingService(), false);
         await expect(ce.cacheGet('foo')).resolves.toBeNull();
         await ce.cachePut('foo', {res: 'bar'}, undefined);
         await expect(ce.cacheGet('foo')).resolves.toBeNull();
     });
     it('Should filter bad options', () => {
         // TODO: Work will need to be done here when CompilationEnvironment's constructor is typed better
-        const ce = new CompilationEnvironment(compilerProps, fakeProps({}), undefined, undefined);
+        const ce = new CompilationEnvironment(
+            compilerProps,
+            fakeProps({}),
+            undefined,
+            new FormattingService(),
+            undefined,
+        );
         expect(ce.findBadOptions(['-O3', '-flto'])).toEqual([]);
         expect(ce.findBadOptions(['-O3', '-plugin'])).toEqual(['-plugin']);
     });
diff --git a/test/handlers/api-tests.ts b/test/handlers/api-tests.ts
index 92194b165..c9adbc649 100644
--- a/test/handlers/api-tests.ts
+++ b/test/handlers/api-tests.ts
@@ -164,23 +164,4 @@ describe('API handling', () => {
             .expect('Content-Type', /json/)
             .expect(200, [languages['c++'], languages.pascal]);
     });
-
-    it('should not go through with invalid tools', async () => {
-        await request(app)
-            .post('/api/format/invalid')
-            .set('Accept', 'application/json')
-            .expect('Content-Type', /json/)
-            .expect(422, {exit: 2, answer: "Unknown format tool 'invalid'"});
-    });
-    it('should not go through with invalid base styles', async () => {
-        await request(app)
-            .post('/api/format/formatt')
-            .send({
-                base: 'bad-base',
-                source: 'i am source',
-            })
-            .set('Accept', 'application/json')
-            .expect(422, {exit: 3, answer: "Style 'bad-base' is not supported"})
-            .expect('Content-Type', /json/);
-    });
 });
diff --git a/test/handlers/api/formatting-controller.tests.ts b/test/handlers/api/formatting-controller.tests.ts
new file mode 100644
index 000000000..f1aee3328
--- /dev/null
+++ b/test/handlers/api/formatting-controller.tests.ts
@@ -0,0 +1,83 @@
+// Copyright (c) 2024, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+import express from 'express';
+import request from 'supertest';
+import {beforeAll, describe, it} from 'vitest';
+
+import {FormattingService} from '../../../lib/formatting-service.js';
+import {FormattingController} from '../../../lib/handlers/api/formatting-controller.js';
+import {fakeProps} from '../../../lib/properties.js';
+
+describe('FormattingController', () => {
+    let app;
+
+    beforeAll(async () => {
+        app = express();
+        const formattingService = new FormattingService();
+        const formattingController = new FormattingController(formattingService);
+        await formattingService.initialize(
+            fakeProps({
+                formatters: 'formatt:badformatt',
+                'formatter.formatt.exe': process.platform === 'win32' ? 'cmd' : 'echo',
+                'formatter.formatt.type': 'clangformat',
+                'formatter.formatt.version': 'Release',
+                'formatter.formatt.name': 'FormatT',
+            }),
+        );
+        app.use(express.json());
+        app.use(formattingController.createRouter());
+    });
+
+    it('should not go through with invalid tools', async () => {
+        await request(app)
+            .post('/api/format/invalid')
+            .set('Accept', 'application/json')
+            .expect('Content-Type', /json/)
+            .expect(422, {exit: 2, answer: "Unknown format tool 'invalid'"});
+    });
+
+    it('should not go through with invalid base styles', async () => {
+        await request(app)
+            .post('/api/format/formatt')
+            .send({
+                base: 'bad-base',
+                source: 'i am source',
+            })
+            .set('Accept', 'application/json')
+            .expect(422, {exit: 3, answer: "Style 'bad-base' is not supported"})
+            .expect('Content-Type', /json/);
+    });
+
+    it('should reject requests with no source', async () => {
+        await request(app)
+            .post('/api/format/formatt')
+            .send({
+                base: 'bad-base',
+            })
+            .set('Accept', 'application/json')
+            .expect(400, {exit: 0, answer: ''})
+            .expect('Content-Type', /json/);
+    });
+});
diff --git a/test/handlers/middleware-tests.ts b/test/handlers/middleware-tests.ts
new file mode 100644
index 000000000..65b1bc9ab
--- /dev/null
+++ b/test/handlers/middleware-tests.ts
@@ -0,0 +1,50 @@
+// Copyright (c) 2024, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+import express from 'express';
+import request from 'supertest';
+import {describe, it} from 'vitest';
+
+import {cached, cors} from '../../lib/handlers/middleware.js';
+
+describe('middleware functions', () => {
+    it('adds cache controls headers with cached()', async () => {
+        const app = express();
+        app.use('/', cached, (_, res) => res.send('Hello World!'));
+        await request(app)
+            .get('/')
+            .expect(200, 'Hello World!')
+            .expect('cache-control', /public, max-age=\d+, must-revalidate/);
+    });
+
+    it('adds cors headers with cors()', async () => {
+        const app = express();
+        app.use('/', cors, (_, res) => res.send('Hello World!'));
+        await request(app)
+            .get('/')
+            .expect(200, 'Hello World!')
+            .expect('access-control-allow-origin', '*')
+            .expect('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept');
+    });
+});