From b272efdb53acdf0434f0899dfc3810e4811e2bac Mon Sep 17 00:00:00 2001 From: Joshua Batty Date: Tue, 25 Feb 2025 02:38:13 +1100 Subject: [PATCH] Add sway compiler (#7409) Should be pretty self explanatory. I've added a corresponding PR to the infra repo [here](https://github.com/compiler-explorer/infra/pull/1532) Please let me know if there is anything I've done incorrectly here! Thanks --------- Co-authored-by: Matt Godbolt --- CONTRIBUTORS.md | 1 + etc/config/sway.amazon.properties | 19 ++ etc/config/sway.defaults.properties | 28 +++ examples/sway/default.sw | 10 ++ lib/compilers/_all.ts | 1 + lib/compilers/sway.ts | 268 ++++++++++++++++++++++++++++ lib/languages.ts | 12 ++ static/modes/_all.ts | 1 + static/modes/sway-mode.ts | 208 +++++++++++++++++++++ types/languages.interfaces.ts | 1 + views/resources/logos/sway.svg | 53 ++++++ 11 files changed, 602 insertions(+) create mode 100644 etc/config/sway.amazon.properties create mode 100644 etc/config/sway.defaults.properties create mode 100644 examples/sway/default.sw create mode 100644 lib/compilers/sway.ts create mode 100644 static/modes/sway-mode.ts create mode 100644 views/resources/logos/sway.svg 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/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/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 = ''; + 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, + ): Promise { + // 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: ''}]; + } 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 = { 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/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: /[=>](?!@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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +