Repository: compiler-explorer/compiler-explorer
Pull request #7413 (2025-02-19 19:56:57)

Test the numba wrapper in test-and-deploy
- Resolve comments from #5592:
  - Replace a silly indirect import https://github.com/compiler-explorer/compiler-explorer/pull/5592#discussion_r1962004963
  - Run `test_numba_wrapper` in `.github/workflows/test-and-deploy.yml`. https://github.com/compiler-explorer/compiler-explorer/pull/5592#discussion_r1962004131
- Patch minor errors in `etc/scripts/util/propschecktest.py` and also test it. 


Patch:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 2854e0f23..969be39db 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -155,3 +155,4 @@ From oldest to newest contributor, we would like to thank:
 - [Vipul Cariappa](https://github.com/Vipul-Cariappa)
 - [Niles Salter](https://github.com/Validark)
 - [Stephen Heumann](https://github.com/sheumann)
+- [Joshua Batty](https://github.com/joshuabatty)
diff --git a/etc/config/c++.amazon.properties b/etc/config/c++.amazon.properties
index 6721b3b87..62b659a53 100644
--- a/etc/config/c++.amazon.properties
+++ b/etc/config/c++.amazon.properties
@@ -5304,7 +5304,7 @@ libs.qt.versions.680.path=/app/qt/include/QtCore
 
 libs.quill.name=Quill
 libs.quill.description=C++ Low Latency Logging Library
-libs.quill.versions=611:700:721:730:750:810
+libs.quill.versions=611:700:721:730:750:810:820
 libs.quill.url=https://github.com/odygrd/quill
 libs.quill.versions.611.version=6.1.1
 libs.quill.versions.611.path=/opt/compiler-explorer/libs/quill/v6.1.1/quill/include
@@ -5318,6 +5318,8 @@ libs.quill.versions.750.version=7.5.0
 libs.quill.versions.750.path=/opt/compiler-explorer/libs/quill/v7.5.0/include
 libs.quill.versions.810.version=8.1.0
 libs.quill.versions.810.path=/opt/compiler-explorer/libs/quill/v8.1.0/include
+libs.quill.versions.820.version=8.2.0
+libs.quill.versions.820.path=/opt/compiler-explorer/libs/quill/v8.2.0/include
 
 libs.rangesv3.name=range-v3
 libs.rangesv3.versions=trunk:030:035:036:091:0100:0110:0120
diff --git a/etc/config/rust.amazon.properties b/etc/config/rust.amazon.properties
index 9d3a38db1..a7e3b0fe1 100644
--- a/etc/config/rust.amazon.properties
+++ b/etc/config/rust.amazon.properties
@@ -2,14 +2,14 @@ compilers=&rust:&rustgcc:&mrustc:&rustccggcc
 objdumper=/opt/compiler-explorer/gcc-14.1.0/bin/objdump
 linker=/opt/compiler-explorer/gcc-14.1.0/bin/gcc
 aarch64linker=/opt/compiler-explorer/arm64/gcc-14.1.0/aarch64-unknown-linux-gnu/bin/aarch64-unknown-linux-gnu-gcc
-defaultCompiler=r1840
+defaultCompiler=r1850
 demangler=/opt/compiler-explorer/gcc-14.1.0/bin/c++filt
 demanglerArgs=--format=rust
 
 buildenvsetup=ceconan-rust
 buildenvsetup.host=https://conan.compiler-explorer.com
 
-group.rust.compilers=r1840:r1830:r1820:r1810:r1800:r1790:r1780:r1770:r1760:r1750:r1740:r1730:r1720:r1710:r1700:r1690:r1680:r1670:r1660:r1650:r1640:r1630:r1620:r1610:r1600:r1590:r1580:r1570:r1560:r1550:r1540:r1530:r1520:r1510:r1500:r1490:r1480:r1470:r1460:r1452:r1450:r1440:r1430:r1420:r1410:r1400:r1390:r1380:r1370:r1360:r1350:r1340:r1330:r1320:r1310:r1300:r1290:r1280:r1271:r1270:r1260:r1250:r1240:r1230:r1220:r1210:r1200:r1190:r1180:r1170:r1160:r1151:r1140:r1130:r1120:r1110:r1100:r190:r180:r170:r160:r150:r140:r130:r120:r110:r100:nightly:beta
+group.rust.compilers=r1850:r1840:r1830:r1820:r1810:r1800:r1790:r1780:r1770:r1760:r1750:r1740:r1730:r1720:r1710:r1700:r1690:r1680:r1670:r1660:r1650:r1640:r1630:r1620:r1610:r1600:r1590:r1580:r1570:r1560:r1550:r1540:r1530:r1520:r1510:r1500:r1490:r1480:r1470:r1460:r1452:r1450:r1440:r1430:r1420:r1410:r1400:r1390:r1380:r1370:r1360:r1350:r1340:r1330:r1320:r1310:r1300:r1290:r1280:r1271:r1270:r1260:r1250:r1240:r1230:r1220:r1210:r1200:r1190:r1180:r1170:r1160:r1151:r1140:r1130:r1120:r1110:r1100:r190:r180:r170:r160:r150:r140:r130:r120:r110:r100:nightly:beta
 group.rust.compilerType=rust
 group.rust.isSemVer=true
 group.rust.unwiseOptions=-Ctarget-cpu=native|target-cpu=native
@@ -19,6 +19,11 @@ group.rust.licenseName=Dual-licensed under Apache 2.0 and MIT terms
 group.rust.licensePreamble=Rust is licensed under the Apache License, Version 2.0 or the MIT license
 group.rust.supportsBinary=true
 group.rust.supportsBinaryObject=true
+# Rust 1.83+ needs `libPath` for execution with dynamically linked standard library
+# see https://github.com/compiler-explorer/compiler-explorer/pull/7367
+compiler.r1850.exe=/opt/compiler-explorer/rust-1.85.0/bin/rustc
+compiler.r1850.semver=1.85.0
+compiler.r1850.libPath=${exePath}/../lib/rustlib/x86_64-unknown-linux-gnu/lib
 compiler.r1840.exe=/opt/compiler-explorer/rust-1.84.0/bin/rustc
 compiler.r1840.semver=1.84.0
 compiler.r1840.libPath=${exePath}/../lib/rustlib/x86_64-unknown-linux-gnu/lib
diff --git a/etc/config/spice.amazon.properties b/etc/config/spice.amazon.properties
index 858d7b02b..06a9c0e61 100644
--- a/etc/config/spice.amazon.properties
+++ b/etc/config/spice.amazon.properties
@@ -1,7 +1,7 @@
 compilers=&spice
-defaultCompiler=spice02101
+defaultCompiler=spice02200
 
-group.spice.compilers=spice01902:spice01903:spice01904:spice01905:spice01906:spice02000:spice02001:spice02002:spice02003:spice02004:spice02005:spice02006:spice02100:spice02101
+group.spice.compilers=spice01902:spice01903:spice01904:spice01905:spice01906:spice02000:spice02001:spice02002:spice02003:spice02004:spice02005:spice02006:spice02100:spice02101:spice02200
 group.spice.demangler=/opt/compiler-explorer/gcc-14.2.0/bin/c++filt
 group.spice.objdumper=/opt/compiler-explorer/gcc-14.2.0/bin/objdump
 group.spice.isSemVer=true
@@ -43,6 +43,8 @@ compiler.spice02100.exe=/opt/compiler-explorer/spice-0.21.0/spice
 compiler.spice02100.semver=0.21.0
 compiler.spice02101.exe=/opt/compiler-explorer/spice-0.21.1/spice
 compiler.spice02101.semver=0.21.1
+compiler.spice02200.exe=/opt/compiler-explorer/spice-0.22.0/spice
+compiler.spice02200.semver=0.22.0
 
 #################################
 #################################
diff --git a/etc/config/sway.amazon.properties b/etc/config/sway.amazon.properties
new file mode 100644
index 000000000..408ae7f58
--- /dev/null
+++ b/etc/config/sway.amazon.properties
@@ -0,0 +1,19 @@
+compilers=&sway
+defaultCompiler=swayv0667
+objdumper=/opt/compiler-explorer/gcc-14.2.0/bin/objdump
+
+group.sway.compilers=swayv0667
+group.sway.compilerType=sway-compiler
+group.sway.isSemVer=true
+group.sway.supportsIrView=true
+group.sway.irArg=--ir final
+group.sway.supportsBinary=true
+group.sway.supportsAsm=true
+group.sway.asmArg=--asm all
+
+compiler.swayv0667.exe=/opt/compiler-explorer/sway-0.66.7/forc-binaries/forc
+compiler.swayv0667.semver=0.66.7
+compiler.swayv0667.name=sway 0.66.7
+
+# Basic tools that might be useful
+tools=
diff --git a/etc/config/sway.defaults.properties b/etc/config/sway.defaults.properties
new file mode 100644
index 000000000..b6e4fef5e
--- /dev/null
+++ b/etc/config/sway.defaults.properties
@@ -0,0 +1,28 @@
+# Basic language definition
+language.sway.name=sway
+language.sway.extensions=.sw
+language.sway.monaco=sway
+
+# Add IR language definition
+language.sway-ir.name=Sway IR
+language.sway-ir.monaco=rust
+
+# Compiler settings
+compilers=&sway
+defaultCompiler=swayv0667
+compilerType=sway-compiler
+
+# Define compiler setup
+group.sway.compilers=swayv0667
+group.sway.groupName=Sway compilers
+group.sway.compilerType=sway-compiler
+group.sway.supportsIrView=true
+group.sway.irArg=--ir final
+group.sway.supportsBinary=true
+group.sway.supportsAsm=true
+group.sway.asmArg=--asm all
+group.sway.versionFlag=--version
+group.sway.isSemVer=true
+
+# No libs support yet
+libs=
diff --git a/etc/config/swift.defaults.properties b/etc/config/swift.defaults.properties
index d1cb80eea..34ed8aa86 100644
--- a/etc/config/swift.defaults.properties
+++ b/etc/config/swift.defaults.properties
@@ -4,6 +4,7 @@ supportsBinary=true
 supportsExecute=true
 compilerType=swift
 intelAsm=-emit-assembly -Xllvm --x86-asm-syntax=intel
+needsMulti=false
 
 
 #################################
diff --git a/etc/scripts/util/propscheck.py b/etc/scripts/util/propscheck.py
index 7e9b242f3..7182e060d 100644
--- a/etc/scripts/util/propscheck.py
+++ b/etc/scripts/util/propscheck.py
@@ -27,6 +27,7 @@ from os import listdir
 from os.path import isfile, join
 import re
 import argparse
+from argparse import Namespace
 
 parser = argparse.ArgumentParser(description='Checks for incorrect/suspicious properties.')
 parser.add_argument ('--check-suspicious-in-default-prop', required=False, action="store_true")
@@ -101,7 +102,7 @@ def check_suspicious_path_and_add(line: Line, m, s):
         s.add(Line(line.number, m.group(2)))
 
 
-def process_file(file: str, args):
+def process_file(file: str, args: Namespace):
     default_compiler = set()
 
     listed_groups = set()
@@ -270,7 +271,7 @@ def print_issue(name, result):
         print(f"{name}:\n  {sep.join(sorted([str(issue) for issue in result]))}")
 
 
-def find_orphans(args: dict):
+def find_orphans(args: Namespace):
     folder = args.config_dir
     result = sorted([(f, r) for (f, r) in process_folder(folder, args) if problems_found(r)], key=lambda x: x[0])
     if result:
diff --git a/examples/sway/default.sw b/examples/sway/default.sw
new file mode 100644
index 000000000..a3e7a1514
--- /dev/null
+++ b/examples/sway/default.sw
@@ -0,0 +1,10 @@
+script;
+
+fn square(x: u64) -> u64 {
+    x * x
+}
+
+fn main() {
+    let x = square(5);
+    assert(x == 25);
+}
\ No newline at end of file
diff --git a/lib/compilers/_all.ts b/lib/compilers/_all.ts
index ffc8751f5..affa32023 100644
--- a/lib/compilers/_all.ts
+++ b/lib/compilers/_all.ts
@@ -136,6 +136,7 @@ export {SnowballCompiler} from './snowball.js';
 export {SolidityCompiler} from './solidity.js';
 export {SolidityZKsyncCompiler} from './solidity-zksync.js';
 export {SpiceCompiler} from './spice.js';
+export {SwayCompiler} from './sway.js';
 export {SwiftCompiler} from './swift.js';
 export {TIC2000} from './tic2000.js';
 export {TableGenCompiler} from './tablegen.js';
diff --git a/lib/compilers/sway.ts b/lib/compilers/sway.ts
new file mode 100644
index 000000000..d84317ec1
--- /dev/null
+++ b/lib/compilers/sway.ts
@@ -0,0 +1,268 @@
+// Copyright (c) 2025, 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 'node:path';
+
+import fs from 'fs-extra';
+
+import {CompilationResult, ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js';
+import {LLVMIrBackendOptions} from '../../types/compilation/ir.interfaces.js';
+import {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
+import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
+import {ResultLine} from '../../types/resultline/resultline.interfaces.js';
+import {BaseCompiler} from '../base-compiler.js';
+import {CompilationEnvironment} from '../compilation-env.js';
+
+interface SymbolMap {
+    paths: string[];
+    map: {
+        [key: string]: {
+            path: number;
+            range: {
+                start: {line: number; col: number};
+                end: {line: number; col: number};
+            };
+        };
+    };
+}
+
+export class SwayCompiler extends BaseCompiler {
+    static get key() {
+        return 'sway-compiler';
+    }
+
+    constructor(info: PreliminaryCompilerInfo, env: CompilationEnvironment) {
+        super(info, env);
+        this.compiler.supportsIrView = true;
+        this.compiler.irArg = ['build', '--ir', 'final'];
+        this.compiler.supportsIntel = true;
+    }
+
+    override async checkOutputFileAndDoPostProcess(asmResult: CompilationResult): Promise<[any, any[], any[]]> {
+        return [asmResult, [], []];
+    }
+
+    override async processAsm(result: any) {
+        // If compilation failed or we have no assembly, return as is
+        if (result.code !== 0 || !result.asm || result.asm.length === 0) {
+            result.asm = '<Compilation failed>';
+            return result;
+        }
+        // The asm array should already be properly formatted from runCompiler
+        return {
+            asm: result.asm,
+            labelDefinitions: {},
+        };
+    }
+
+    override async generateIR(
+        inputFilename: string,
+        options: string[],
+        irOptions: LLVMIrBackendOptions,
+        produceCfg: boolean,
+        filters: ParseFiltersAndOutputOptions,
+    ) {
+        // We can use runCompiler since it already handles all the project setup
+        const result = await this.runCompiler(
+            this.compiler.exe,
+            ['build', '--ir', 'final'],
+            inputFilename,
+            this.getDefaultExecOptions(),
+            filters,
+        );
+
+        return {
+            code: result.code,
+            stdout: [],
+            stderr: result.stderr,
+            asm: result.irOutput?.asm || [],
+            timedOut: result.timedOut,
+            execTime: result.execTime,
+            okToCache: true,
+            inputFilename: result.inputFilename,
+            dirPath: result.dirPath,
+        };
+    }
+
+    override optionsForFilter(filters: ParseFiltersAndOutputOptions, outputFilename: string): string[] {
+        // return an array of command line options for the compiler
+        return ['-o', outputFilename];
+    }
+
+    override async runCompiler(
+        compiler: string,
+        options: string[],
+        inputFilename: string,
+        execOptions: ExecutionOptionsWithEnv,
+        filters?: Partial<ParseFiltersAndOutputOptions>,
+    ): Promise<CompilationResult> {
+        // Make a temp directory for a forc project
+        const projectDir = await this.newTempDir();
+        const {symbolsPath} = await setupForcProject(projectDir, inputFilename);
+
+        // Run `forc build`
+        // "compiler" is the path to the forc binary from .properties
+        const buildResult = await this.exec(compiler, ['build', '-g', symbolsPath], {
+            ...execOptions,
+            customCwd: projectDir,
+        });
+
+        // If build succeeded, parse the bytecode
+        let asm: ResultLine[] = [];
+        if (buildResult.code === 0) {
+            const artifactPath = path.join(projectDir, 'out', 'debug', 'compiler-explorer.bin');
+
+            if (filters?.intel) {
+                const asmResult = await this.exec(compiler, ['build', '--asm', 'all'], {
+                    ...execOptions,
+                    customCwd: projectDir,
+                });
+                const lines = splitLines(asmResult.stdout);
+                const startIndex = lines.findIndex(line => line.includes(';; ASM: Virtual abstract program'));
+                const endIndex = lines.findIndex(line => line.includes('[1;32mFinished'));
+                if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
+                    asm = [{text: '<Error extracting ASM output>'}];
+                } else {
+                    asm = lines
+                        .slice(startIndex, endIndex)
+                        .filter(line => line.trim() !== '')
+                        .map(line => ({text: line}));
+                }
+            } else {
+                const parseResult = await this.exec(compiler, ['parse-bytecode', artifactPath], {
+                    ...execOptions,
+                    customCwd: projectDir,
+                });
+                let symbols: SymbolMap | undefined;
+                if (await fs.pathExists(symbolsPath)) {
+                    const symbolsContent = await fs.readFile(symbolsPath, 'utf8');
+                    symbols = JSON.parse(symbolsContent);
+                }
+
+                // Map the bytecode lines
+                const contentLines = splitLines(parseResult.stdout)
+                    .filter(line => line.trim() !== '')
+                    .map(line => {
+                        const match = line.match(/^\s*(\d+)\s+(\d+)\s+/);
+                        if (match && symbols) {
+                            const opcodeIndex = match[1];
+                            const symbolInfo = symbols.map[opcodeIndex];
+                            if (symbolInfo && symbolInfo.path === 1) {
+                                return {
+                                    text: line,
+                                    source: {
+                                        file: symbols.paths[symbolInfo.path],
+                                        line: symbolInfo.range.start.line,
+                                        column: symbolInfo.range.start.col,
+                                        mainsource: true,
+                                    },
+                                };
+                            }
+                        }
+                        return {text: line};
+                    });
+                asm.push(...contentLines);
+            }
+        }
+
+        // Run `forc build --ir final` to gather IR output and store it in `result.irOutput`.
+        let irLines: ResultLine[] = [];
+        if (buildResult.code === 0) {
+            const irResult = await this.exec(compiler, ['build', '--ir', 'final'], {
+                ...execOptions,
+                customCwd: projectDir,
+            });
+            const lastIrMarkerIndex = irResult.stdout.lastIndexOf('// IR: Final');
+            if (lastIrMarkerIndex >= 0) {
+                const relevantIr =
+                    irResult.stdout
+                        .slice(lastIrMarkerIndex)
+                        .split('\n')
+                        .slice(1)
+                        .join('\n')
+                        .match(/(script|library|contract|predicate)\s*{.*?^}/ms)?.[0] || '';
+                irLines = relevantIr.split('\n').map(line => ({text: line}));
+            }
+        }
+
+        // Construct and return a CompilationResult
+        const result: CompilationResult = {
+            code: buildResult.code,
+            timedOut: buildResult.timedOut ?? false,
+            stdout: splitLines(buildResult.stdout).map(line => ({text: line})),
+            stderr: splitLines(buildResult.stderr).map(line => ({text: line})),
+            asm,
+            inputFilename,
+            execTime: buildResult.execTime,
+            okToCache: true,
+            dirPath: projectDir,
+            irOutput:
+                irLines.length > 0
+                    ? {
+                          asm: irLines.map(line => ({
+                              text: line.text,
+                          })),
+                      }
+                    : undefined,
+        };
+
+        return result;
+    }
+}
+
+const FORC_TOML_CONTENT = `[project]
+entry = "main.sw"
+license = "Apache-2.0"
+name = "compiler-explorer"
+
+[dependencies]
+`;
+
+async function setupForcProject(
+    projectDir: string,
+    inputFilename: string,
+): Promise<{mainSw: string; symbolsPath: string}> {
+    const outDebugDir = path.join(projectDir, 'out', 'debug');
+    const symbolsPath = path.join(outDebugDir, 'symbols.json');
+    await fs.mkdirp(outDebugDir);
+
+    // Write Forc.toml file
+    const forcTomlPath = path.join(projectDir, 'Forc.toml');
+    await fs.writeFile(forcTomlPath, FORC_TOML_CONTENT);
+
+    // Copy input file to src/main.sw
+    const srcDir = path.join(projectDir, 'src');
+    await fs.mkdirp(srcDir);
+    const mainSw = path.join(srcDir, 'main.sw');
+    await fs.copyFile(inputFilename, mainSw);
+
+    return {mainSw, symbolsPath};
+}
+
+/**
+ * Splits a multi-line string into an array of lines, omitting the trailing newline if present.
+ */
+function splitLines(str: string): string[] {
+    return str.split(/\r?\n/);
+}
diff --git a/lib/languages.ts b/lib/languages.ts
index 96377174c..9a525a0f1 100644
--- a/lib/languages.ts
+++ b/lib/languages.ts
@@ -928,6 +928,18 @@ const definitions: Record<LanguageKey, LanguageDefinition> = {
         previewFilter: null,
         monacoDisassembly: null,
     },
+    sway: {
+        name: 'sway',
+        monaco: 'sway',
+        extensions: ['.sw'],
+        alias: [],
+        logoUrl: 'sway.svg',
+        logoUrlDark: null,
+        formatter: null,
+        previewFilter: null,
+        monacoDisassembly: null,
+        digitSeparator: '_',
+    },
 };
 
 export const languages = Object.fromEntries(
diff --git a/static/modes/_all.ts b/static/modes/_all.ts
index 6e8a641be..e5e201ec2 100644
--- a/static/modes/_all.ts
+++ b/static/modes/_all.ts
@@ -63,6 +63,7 @@ import './ptx-mode';
 import './slang-mode';
 import './spice-mode';
 import './spirv-mode';
+import './sway-mode';
 import './tablegen-mode';
 import './v-mode';
 import './vala-mode';
diff --git a/static/modes/spice-mode.ts b/static/modes/spice-mode.ts
index 980a7b852..226d9060a 100644
--- a/static/modes/spice-mode.ts
+++ b/static/modes/spice-mode.ts
@@ -1,4 +1,4 @@
-// Copyright (c) 2023, Marc Auberer
+// Copyright (c) 2023-2025, Marc Auberer
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without
@@ -35,6 +35,7 @@ function definition(): monaco.languages.IMonarchLanguage {
             'assert',
             'break',
             'case',
+            'cast',
             'compose',
             'const',
             'continue',
diff --git a/static/modes/sway-mode.ts b/static/modes/sway-mode.ts
new file mode 100644
index 000000000..f785b3dac
--- /dev/null
+++ b/static/modes/sway-mode.ts
@@ -0,0 +1,208 @@
+// Copyright (c) 2025, 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 * as monaco from 'monaco-editor';
+
+function definition(): monaco.languages.IMonarchLanguage {
+    return {
+        keywords: [
+            'abi',
+            'as',
+            'asm',
+            'break',
+            'configurable',
+            'const',
+            'contract',
+            'continue',
+            'deref',
+            'else',
+            'enum',
+            'fn',
+            'for',
+            'if',
+            'impl',
+            'let',
+            'library',
+            'match',
+            'mod',
+            'mut',
+            'predicate',
+            'pub',
+            'ref',
+            'return',
+            'script',
+            'self',
+            'storage',
+            'struct',
+            'trait',
+            'type',
+            'use',
+            'where',
+            'while',
+        ],
+
+        typeKeywords: [
+            'u8',
+            'u16',
+            'u32',
+            'u64',
+            'u128',
+            'u256',
+            'i8',
+            'i16',
+            'i32',
+            'i64',
+            'i128',
+            'i256',
+            'b256',
+            'bool',
+            'str',
+            'Self',
+        ],
+
+        operators: [
+            '=',
+            '>',
+            '<',
+            '!',
+            '~',
+            '?',
+            ':',
+            '==',
+            '<=',
+            '>=',
+            '!=',
+            '&&',
+            '||',
+            '++',
+            '--',
+            '+',
+            '-',
+            '*',
+            '/',
+            '&',
+            '|',
+            '^',
+            '%',
+            '<<',
+            '>>',
+            '>>>',
+            '+=',
+            '-=',
+            '*=',
+            '/=',
+            '&=',
+            '|=',
+            '^=',
+            '%=',
+            '<<=',
+            '>>=',
+            '>>>=',
+        ],
+
+        symbols: /[=><!~?:&|+\-*\/\^%]+/,
+
+        escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
+
+        tokenizer: {
+            root: [
+                // identifiers and keywords
+                [
+                    /[a-z_$][\w$]*/,
+                    {
+                        cases: {
+                            '@typeKeywords': 'keyword.type',
+                            '@keywords': 'keyword',
+                            '@default': 'identifier',
+                        },
+                    },
+                ],
+                [/[A-Z][\w$]*/, 'type.identifier'],
+
+                // whitespace
+                {include: '@whitespace'},
+
+                // delimiters and operators
+                [/[{}()\[\]]/, '@brackets'],
+                [/[<>](?!@symbols)/, '@brackets'],
+                [
+                    /@symbols/,
+                    {
+                        cases: {
+                            '@operators': 'operator',
+                            '@default': '',
+                        },
+                    },
+                ],
+
+                // numbers
+                [/\d*\.\d+([eE][-+]?\d+)?/, 'number.float'],
+                [/0[xX][0-9a-fA-F]+/, 'number.hex'],
+                [/\d+/, 'number'],
+
+                // delimiter: after number because of .\d floats
+                [/[;,.]/, 'delimiter'],
+
+                // strings
+                [/"([^"\\]|\\.)*$/, 'string.invalid'],
+                [
+                    /"/,
+                    {
+                        token: 'string.quote',
+                        bracket: '@open',
+                        next: '@string',
+                    },
+                ],
+
+                // characters
+                [/'[^\\']'/, 'string'],
+                [/(')(@escapes)(')/, ['string', 'string.escape', 'string']],
+                [/'/, 'string.invalid'],
+            ],
+
+            comment: [
+                [/[^/*]+/, 'comment'],
+                [/\/\*/, 'comment', '@push'],
+                ['\\*/', 'comment', '@pop'],
+                [/[/*]/, 'comment'],
+            ],
+
+            string: [
+                [/[^\\"]+/, 'string'],
+                [/@escapes/, 'string.escape'],
+                [/\\./, 'string.escape.invalid'],
+                [/"/, {token: 'string.quote', bracket: '@close', next: '@pop'}],
+            ],
+
+            whitespace: [
+                [/[ \t\r\n]+/, 'white'],
+                [/\/\*/, 'comment', '@comment'],
+                [/\/\/.*$/, 'comment'],
+            ],
+        },
+    };
+}
+
+monaco.languages.register({id: 'sway'});
+monaco.languages.setMonarchTokensProvider('sway', definition());
diff --git a/types/languages.interfaces.ts b/types/languages.interfaces.ts
index 7a3bebca1..490e086da 100644
--- a/types/languages.interfaces.ts
+++ b/types/languages.interfaces.ts
@@ -90,6 +90,7 @@ export type LanguageKey =
     | 'solidity'
     | 'spice'
     | 'spirv'
+    | 'sway'
     | 'swift'
     | 'tablegen'
     | 'toit'
diff --git a/views/resources/logos/sway.svg b/views/resources/logos/sway.svg
new file mode 100644
index 000000000..3e39af1ee
--- /dev/null
+++ b/views/resources/logos/sway.svg
@@ -0,0 +1,53 @@
+<svg width="834" height="907" viewBox="0 0 834 907" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M491.685 799.437L447.013 804.777L396.611 795.229L375.993 783.017L391.271 873.122L409.975 893.359L461.139 906.332L508.856 899.46L491.685 799.437Z" fill="#74402F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M513.855 784.527L490.943 799.043L508.505 899.828L529.123 882.265L513.855 784.527Z" fill="#744534"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M505.42 776.122L486.716 788.725L443.577 794.065L401.961 785.289L384.399 773.828L375.993 783.375L397.763 796.74L446.633 805.897L491.294 800.557L513.826 784.898L505.42 776.122Z" fill="#7B4A38"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M476.043 698.259L433.285 702.457L395.486 694.823L371.812 681.078L384.035 774.228L401.216 785.689L442.822 794.846L487.494 789.506L476.043 698.259Z" fill="#74402F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M492.884 686.791L475.702 698.252L487.153 788.738L505.096 777.277L492.884 686.791Z" fill="#744534"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.484 679.162L472.978 689.09L431.361 692.526L397.379 687.557L380.589 674.964L371.422 681.075L395.476 696.724L433.275 702.835L474.891 699.018L493.215 686.796L487.484 679.162Z" fill="#7B4A38"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.533 572.252L460.016 579.896L429.861 582.561L396.259 577.982L381.753 570.348L380.22 675.721L397.401 688.324L432.536 694.045L473.38 689.466L487.125 679.538L474.533 572.252Z" fill="#74402F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M469.909 565.375L456.545 573.02L428.674 575.684L399.661 570.345L386.677 564.233L381.718 570.725L395.844 579.121L429.445 582.938L459.6 580.654L474.498 572.629L469.909 565.375Z" fill="#7B4A38"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M464.987 448.928L437.115 451.984L411.148 450.451L399.316 444.73L387.094 564.61L400.078 571.482L428.329 576.061L456.972 573.396L469.945 565.752L477.97 442.817L464.987 448.928Z" fill="#74402F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M456.561 326.766L424.111 320.274L402.352 439.773L433.278 458.868L461.53 447.407L473.742 452.757L482.909 322.569L456.561 326.766Z" fill="#74402F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M475.275 437.876L466.869 443.216L440.531 446.652L411.899 444.749L402.352 439.399L399.296 445.13L411.127 450.85L440.151 452.383L467.291 449.327L477.95 442.835L475.275 437.876Z" fill="#7B4A38"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M482.555 318.347L474.335 322.174L455.62 324.592L432.986 323.08L425.445 318.851L423.377 323.316L432.379 327.905L455.322 329.119L476.773 326.701L482.936 322.936L482.555 318.347Z" fill="#7B4A38"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.509 571.858L459.23 581.025L473.356 689.453L487.101 680.286L474.509 571.858Z" fill="#744534"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M468.004 448.921L457.314 573.008L469.907 566.135L477.428 443.478L468.004 448.921Z" fill="#744534"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M476.798 326.758L467.25 443.582L474.514 438.623L482.909 322.18L476.798 326.758Z" fill="#744534"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M492.853 195.046C492.102 195.592 471.854 181.682 471.854 181.682L450.856 189.316L423.746 319.123L424.888 319.504L434.436 324.082L455.815 324.854L475.29 322.94L482.544 318.362L490.178 244.677C490.178 244.677 493.604 194.501 492.853 195.046Z" fill="#74402F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M482.912 268.341L476.42 322.561L482.531 318.353L487.871 272.539L482.912 268.341Z" fill="#744534"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M177.536 292.386L142.412 518.41L236.334 314.907L177.536 292.386Z" fill="#32B12E"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M234.431 315.681L142.412 518.031L160.355 533.68L331.789 394.705L234.431 315.681Z" fill="#279523"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M332.901 231.275L232.868 314.507L330.988 395.455L426.823 320.238L332.901 231.275Z" fill="#29B825"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M269.546 216.794L176.766 292.393L234.03 316.066L333.683 231.301L269.546 216.794Z" fill="#30CB2B"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M431.397 208.013L269.521 216.789L330.602 232.829L426.438 320.639L449.72 243.518L463.856 238.549L464.618 226.337L431.397 208.013Z" fill="#4EE448"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M439.771 5.32934L338.349 0L367.341 31.3073L468.259 38.5194L439.771 5.32934Z" fill="#53E44E"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M467.566 37.7146L367.358 31.3153L442.175 123.704L483.626 121.76L467.566 37.7146Z" fill="#44D83F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M467.16 37.7356L481.348 121.277L497.079 142.347L515.999 81.7901L467.16 37.7356Z" fill="#61E45C"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M442.153 123.352L466.753 193.405L476.897 196.348L481.712 120.904L442.153 123.352Z" fill="#2DAD2A"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M481.369 120.485L476.533 196.31L481.667 199.694L497.233 141.771L481.369 120.485Z" fill="#3BC237"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M510.806 209.533L477.976 214.883L471.104 257.26L549.367 247.332L510.806 209.533Z" fill="#3DE337"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M547.463 246.958L471.104 256.887L507.75 343.936L581.435 343.174L547.463 246.958Z" fill="#3EDC39"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M581.803 343.174L506.977 343.555L574.55 537.88L587.153 529.485L581.803 343.174Z" fill="#2EB42A"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M580.67 342.77L586.781 529.091L601.668 542.075L681.464 384.386L580.67 342.77Z" fill="#289C24"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M586.815 528.338L574.212 537.494L601.702 541.702L586.815 528.338Z" fill="#31912E"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M653.232 303.451L511.202 210.29L547.479 246.948L580.689 342.773L681.484 384.389L653.232 303.451Z" fill="#54E94F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M139.355 48.8041L0 139.67L150.816 64.4527L139.355 48.8041Z" fill="#38CD33"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M150.784 64.0638L0.348633 139.282L4.16559 148.057L167.966 128.211L150.784 64.0638Z" fill="#30B62C"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M139.353 48.8042L150.814 64.8436L293.605 73.2389L139.353 48.8042Z" fill="#46E641"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M293.925 72.8448L150.753 64.4392L167.543 128.587L307.289 146.91L293.925 72.8448Z" fill="#44DE3F"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M160.732 37.7355L139.353 48.8057L203.5 59.1146L160.732 37.7355Z" fill="#3FD43B"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M245.092 14.0555L160.707 37.7289L201.943 59.108L294.722 73.6145L339.014 45.7435L245.092 14.0555Z" fill="#47DF42"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M426.844 141.963L373.016 70.5624L338.653 45.3664L293.601 73.2374C293.601 73.2374 421.505 156.089 444.293 172.149L426.844 141.963Z" fill="#41CF3C"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M414.621 137.38L293.209 72.8622L306.954 146.928L419.971 174.418L461.587 211.075L414.621 137.38Z" fill="#54E450"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M464.62 226.723L461.184 210.303L420.329 174.037L410.781 171.743L414.979 179.767L432.551 201.527L431.78 208.4L464.62 226.723Z" fill="#2BA828"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M772.724 179.379L674.224 145.026L578.779 163.349L552.811 171.745L513.109 161.436L508.139 183.206L494.014 190.84L490.958 203.433L587.174 169.08L717.362 167.928L817.396 206.488L772.724 179.379Z" fill="#5CE857"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M817.043 206.095L717.771 167.534L726.177 224.809L803.679 248.472L809.409 256.116L833.083 261.456L817.043 206.095Z" fill="#51D84C"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M817.024 206.086L804.04 248.464L809.009 256.869L833.444 262.209L817.024 206.086Z" fill="#36BD31"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M717.749 167.915L587.18 169.068L583.363 194.644L726.916 225.19L717.749 167.915Z" fill="#36BD31"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M675.733 283.592L553.56 238.54L652.831 303.448L677.647 373.316L689.859 374.078L720.024 405.385L756.682 376.372L675.733 283.592Z" fill="#3FD43B"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M706.681 293.135L675.374 283.588L755.17 377.129L797.928 373.693L706.681 293.135Z" fill="#5FEC59"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M754.426 376.752L786.875 569.945L797.565 373.696L754.426 376.752Z" fill="#32B12E"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M754.791 377.118L719.286 404.989L778.464 569.55L786.86 569.159L754.791 377.118Z" fill="#31912E"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M490.589 203.046L583.749 194.65L587.566 169.074L490.589 203.046Z" fill="#2EB42A"/>
+</svg>


Test patch:
diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml
index 2d8a54ae6..0c3682ce3 100644
--- a/.github/workflows/test-and-deploy.yml
+++ b/.github/workflows/test-and-deploy.yml
@@ -18,13 +18,17 @@ jobs:
           check-latest: true
           cache: npm
       - name: Install prerequisites
-        run: make prereqs
+        run: |
+          make prereqs
+          python3 -m pip install numba
       - name: Run checks
         run: |
           npm run lint-check
           npm run test
           npm run ts-check
           python3 ./etc/scripts/util/propscheck.py
+          python3 ./etc/scripts/util/propschecktest.py
+          python3 ./etc/scripts/test_numba_wrapper.py
       - uses: codecov/codecov-action@v4
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/etc/scripts/test_numba_wrapper.py b/etc/scripts/test_numba_wrapper.py
index 6e47fe499..4efbef044 100644
--- a/etc/scripts/test_numba_wrapper.py
+++ b/etc/scripts/test_numba_wrapper.py
@@ -30,11 +30,10 @@ import os
 import sys
 import unittest
 import unittest.mock
-
+import tempfile
 import numba
-from numba.core.caching import tempfile
 
-from . import numba_wrapper
+import numba_wrapper
 
 
 class TestMain(unittest.TestCase):
@@ -188,3 +187,6 @@ class TestOpenOrStdout(unittest.TestCase):
     def test_stdout(self) -> None:
         with numba_wrapper._open_or_stdout(None) as file_:
             self.assertIs(file_, sys.stdout)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/etc/scripts/util/propschecktest.py b/etc/scripts/util/propschecktest.py
index e6f665be7..cb9da2f93 100644
--- a/etc/scripts/util/propschecktest.py
+++ b/etc/scripts/util/propschecktest.py
@@ -1,5 +1,6 @@
-import sys
+import argparse
 import os
+import sys
 import unittest
 
 from propscheck import process_file, Line
@@ -9,7 +10,8 @@ class PropsCheckTests(unittest.TestCase):
     def run_test(self, filename, expected_key, expected_contents):
         base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
         test_case_file = os.path.join(base_path, 'test', 'cases', f"{filename}.properties")
-        result = process_file(test_case_file)
+        args = argparse.Namespace(check_suspicious_in_default_prop=True)
+        result = process_file(test_case_file, args)
         self.assertEqual(result[expected_key], {Line(-1, text) for text in expected_contents})
 
     def test_bad_compilers_exe(self):
@@ -77,7 +79,8 @@ class PropsCheckTests(unittest.TestCase):
     def test_good_file(self):
         base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
         test_case_file = os.path.join(base_path, '..', '..', 'config', 'c++.amazon.properties')
-        result = process_file(test_case_file)
+        args = argparse.Namespace(check_suspicious_in_default_prop=False)
+        result = process_file(test_case_file, args)
         for k in result:
             self.assertEqual(result[k], set(), f"{k} has output in known good file")