Add Resolc 0.4.0 compiler for Solidity and Yul (#8164)

## What

Adds [Revive's Resolc](https://github.com/paritytech/revive) compiler
for compiling Solidity and Yul (Solidity IR) to RISC-V and PolkaVM
assembly.

### Main Additions

- [x] Implement new `ResolcCompiler`
- [x] Implement Yul language definition and config for Monaco
- [x] Add Resolc as a compiler for the Solidity and Yul languages
  - The `ResolcCompiler` handles both kinds of language input 
- [x] Implement initial `PolkaVMAsmParser` (no source mappings)
- [x] Enable viewing LLVM IR in a supplementary view
- [x] Implement a new LLVM IR backend option for toggling between
optimized and unoptimized ll
- Affects non-resolc files ([see
commit](606bab9a59))
  - Disabled by default
- (Enable by setting `this.compiler.supportsIrViewOptToggleOption =
true` in a compiler's constructor)
- The compiler's `getIrOutputFilename()` will receive the LLVM IR
backend options

### CE Infra

Accompanying CE Infra PR:
https://github.com/compiler-explorer/infra/pull/1855

## Overall Usage

### Steps

(See screenshots)

* Choose between two input languages:
  * Solidity
  * Yul (Solidity IR)
* Choose a Resolc compiler
* View assembly:
  * PolkaVM assembly (if enabling "Compile to binary")
  * RISC-V (64 bits) assembly
* View intermediate results:
  * Optimized LLVM IR (if enabling "Show Optimized" in the LLVM IR view)
  * Unoptimized LLVM IR

### Notes

Source mappings currently only exist between:
- Yul and RISC-V
- Yul and LLVM-IR

## Screenshots

<img width="1502" height="903" alt="CE Yul RISC-V LLVM IR"
src="https://github.com/user-attachments/assets/7503b9b5-0f2c-4ddf-9405-669e4bdcd02d"
/>

<img width="1502" height="903" alt="CE Solidity PolkaVM"
src="https://github.com/user-attachments/assets/eeb51c99-3eaa-4dda-b13c-ac7783e66cb8"
/>

---------

Co-authored-by: Matt Godbolt <matt@godbolt.org>
This commit is contained in:
LJ
2025-10-14 20:56:59 +02:00
committed by GitHub
parent 55f6469956
commit 066a942cbc
27 changed files with 1440 additions and 13 deletions

8
.github/labeler.yml vendored
View File

@@ -371,6 +371,7 @@
- 'lib/compilers/solidity.ts'
- 'lib/compilers/solidity-zksync.ts'
- 'lib/compilers/solx.ts'
- 'lib/compilers/resolc.ts'
- 'etc/config/solidity.*.properties'
'lang-spice':
@@ -447,6 +448,13 @@
- 'etc/config/wasm.*.properties'
- 'static/modes/wat-mode.ts'
'lang-yul':
- changed-files:
- any-glob-to-any-file:
- 'lib/compilers/resolc.ts'
- 'etc/config/yul.*.properties'
- 'static/modes/yul-mode.ts'
'lang-zig':
- changed-files:
- any-glob-to-any-file:

View File

@@ -165,4 +165,5 @@ From oldest to newest contributor, we would like to thank:
- [Florian Freitag](https://github.com/flofriday)
- [Trevor Gross](https://github.com/tgross35)
- [Alex Trotta](https://github.com/Ahajha)
- [natinusala](https://github.com/natinusala)
- [natinusala](https://github.com/natinusala)
- [LJ](https://github.com/elle-j)

View File

@@ -1,5 +1,6 @@
compilers=&solc:&zksolc:&solx
compilers=&solc:&zksolc:&solx:&resolc
defaultCompiler=solc0830
supportsLibraryCodeFilter=true
group.solc.compilers=solc0426:solc0517:solc0612:solc076:solc0821:solc0829:solc0830
group.solc.compilerType=solidity
@@ -58,6 +59,24 @@ compiler.solx012.exe=/opt/compiler-explorer/solx-0.1.2/solx
compiler.solx012.semver=0.1.2
compiler.solx012.name=solx 0.1.2
# The Resolc compiler supports compiling both Solidity and Yul (Solidity IR), thus
# the same compiler is used for both languages. The compiler config IDs will clash
# if they are not unique, therefore the IDs contain either 'yul' or 'sol'.
group.resolc.compilers=resolc040_sol
group.resolc.compilerType=resolc
group.resolc.objdumper=/opt/compiler-explorer/clang-21.1.0/bin/llvm-objdump
group.resolc.objdumperType=llvm
group.resolc.isSemver=true
group.resolc.versionFlag=--version
group.resolc.intelAsm=false
group.resolc.supportsBinaryObject=true
group.resolc.supportsBinary=false
group.resolc.supportsExecute=false
compiler.resolc040_sol.exe=/opt/compiler-explorer/resolc-0.4.0/resolc
compiler.resolc040_sol.semver=0.4.0
compiler.resolc040_sol.name=resolc 0.4.0
compiler.resolc040_sol.instructionSet=riscv64
#################################
#################################
# Installed libs (See c++.amazon.properties for a scheme of libs group)

View File

@@ -1,6 +1,7 @@
compilers=solc:zksolc:solx
compilers=solc:zksolc:solx:&resolc
compilerType=solidity
defaultCompiler=solc
supportsLibraryCodeFilter=true
compiler.solc.exe=/usr/bin/solc
compiler.solc.semver=0.8.21
@@ -23,6 +24,21 @@ compiler.solx.compilerType=solx
compiler.solx.instructionSet=evm
compiler.solx.isSemVer=true
group.resolc.compilers=resolc040_sol
group.resolc.compilerType=resolc
group.resolc.objdumper=/usr/local/bin/llvm-objdump
group.resolc.objdumperType=llvm
group.resolc.isSemver=true
group.resolc.versionFlag=--version
group.resolc.intelAsm=false
group.resolc.supportsBinaryObject=true
group.resolc.supportsBinary=false
group.resolc.supportsExecute=false
compiler.resolc040_sol.exe=/opt/compiler-explorer/resolc-0.4.0/resolc
compiler.resolc040_sol.semver=0.4.0
compiler.resolc040_sol.name=resolc 0.4.0
compiler.resolc040_sol.instructionSet=riscv64
#################################
#################################
# Installed libs (See c++.amazon.properties for a scheme of libs group)

View File

@@ -0,0 +1,32 @@
compilers=&resolc
defaultCompiler=resolc040_yul
supportsLibraryCodeFilter=true
# The Resolc compiler supports compiling both Solidity and Yul (Solidity IR), thus
# the same compiler is used for both languages. The compiler config IDs will clash
# if they are not unique, therefore the IDs contain either 'yul' or 'sol'.
group.resolc.compilers=resolc040_yul
group.resolc.compilerType=resolc
group.resolc.objdumper=/opt/compiler-explorer/clang-21.1.0/bin/llvm-objdump
group.resolc.objdumperType=llvm
group.resolc.isSemver=true
group.resolc.versionFlag=--version
group.resolc.intelAsm=false
group.resolc.supportsBinaryObject=true
group.resolc.supportsBinary=false
group.resolc.supportsExecute=false
compiler.resolc040_yul.exe=/opt/compiler-explorer/resolc-0.4.0/resolc
compiler.resolc040_yul.semver=0.4.0
compiler.resolc040_yul.name=resolc 0.4.0
compiler.resolc040_yul.instructionSet=riscv64
#################################
#################################
# Installed libs (See c++.amazon.properties for a scheme of libs group)
libs=
#################################
#################################
# Installed tools
tools=

View File

@@ -0,0 +1,24 @@
compilers=&resolc
compilerType=resolc
defaultCompiler=resolc040_yul
supportsLibraryCodeFilter=true
group.resolc.compilers=resolc040_yul
group.resolc.compilerType=resolc
group.resolc.objdumper=/usr/local/bin/llvm-objdump
group.resolc.objdumperType=llvm
group.resolc.isSemver=true
group.resolc.versionFlag=--version
group.resolc.intelAsm=false
group.resolc.supportsBinaryObject=true
group.resolc.supportsBinary=false
group.resolc.supportsExecute=false
compiler.resolc040_yul.exe=/opt/compiler-explorer/resolc-0.4.0/resolc
compiler.resolc040_yul.semver=0.4.0
compiler.resolc040_yul.name=resolc 0.4.0
compiler.resolc040_yul.instructionSet=riscv64
#################################
#################################
# Installed libs (See c++.amazon.properties for a scheme of libs group)
libs=

43
examples/yul/default.yul Normal file
View File

@@ -0,0 +1,43 @@
object "Square" {
code {
{
let _1 := memoryguard(0x80)
mstore(64, _1)
if callvalue() { revert(0, 0) }
let _2 := datasize("Square_deployed")
codecopy(_1, dataoffset("Square_deployed"), _2)
return(_1, _2)
}
}
object "Square_deployed" {
code {
{
let _1 := memoryguard(0x80)
mstore(64, _1)
if iszero(lt(calldatasize(), 4))
{
if eq(0xd27b3841, shr(224, calldataload(0)))
{
if callvalue() { revert(0, 0) }
if slt(add(calldatasize(), not(3)), 32) { revert(0, 0) }
let value := calldataload(4)
let _2 := and(value, 0xffffffff)
if iszero(eq(value, _2)) { revert(0, 0) }
let product_raw := mul(_2, _2)
let product := and(product_raw, 0xffffffff)
if iszero(eq(product, product_raw))
{
mstore(0, shl(224, 0x4e487b71))
mstore(4, 0x11)
revert(0, 0x24)
}
mstore(_1, product)
return(_1, 32)
}
}
revert(0, 0)
}
}
data ".metadata" hex"a26469706673582212209b2b1b86ce0e1a75faa800884ba155bd6bc6a6bc71f210370f818535dcfc5ee364736f6c634300081e0033"
}
}

View File

@@ -1455,7 +1455,7 @@ export class BaseCompiler {
asm: ParsedAsmResultLine[];
languageId: string;
}> {
const irPath = this.getIrOutputFilename(output.inputFilename!, filters);
const irPath = this.getIrOutputFilename(output.inputFilename!, filters, irOptions);
if (await utils.fileExists(irPath)) {
const output = await fs.readFile(irPath, 'utf8');
return await this.llvmIr.process(output, irOptions);
@@ -1701,8 +1701,18 @@ export class BaseCompiler {
return [{text: 'Internal error; unable to open output path'}];
}
getIrOutputFilename(inputFilename: string, filters?: ParseFiltersAndOutputOptions): string {
// filters are passed because rust needs to know whether a binary is being produced or not
/**
* Get the LLVM IR output filename.
*
* @param inputFilename Input filename.
* @param filters Can be used if this base method is overridden. E.g. in order to know whether a binary is being produced (used by Rust).
* @param irOptions Can be used if this base method is overridden. E.g. in order return an output file based on `irOptions` (used by Resolc).
*/
getIrOutputFilename(
inputFilename: string,
filters?: ParseFiltersAndOutputOptions,
irOptions?: LLVMIrBackendOptions,
): string {
return utils.changeExtension(inputFilename, '.ll');
}

View File

@@ -133,6 +133,7 @@ export {QNXCompiler} from './qnx.js';
export {R8Compiler} from './r8.js';
export {RacketCompiler} from './racket.js';
export {RakuCompiler} from './raku.js';
export {ResolcCompiler} from './resolc.js';
export {RGACompiler} from './rga.js';
export {RubyCompiler} from './ruby.js';
export {RustCompiler} from './rust.js';

View File

@@ -942,6 +942,13 @@ export class SolxParser extends RustParser {
}
}
export class ResolcParser extends BaseParser {
override async parse() {
await this.getOptions('--help');
return this.compiler;
}
}
export class MrustcParser extends BaseParser {
override async parse() {
await this.getOptions('--help');

318
lib/compilers/resolc.ts Normal file
View File

@@ -0,0 +1,318 @@
// 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 fs from 'node:fs';
import path from 'node:path';
import type {ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
import type {CompilationResult} from '../../types/compilation/compilation.interfaces.js';
import type {LLVMIrBackendOptions} from '../../types/compilation/ir.interfaces.js';
import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import type {Language} from '../../types/languages.interfaces.js';
import {assert} from '../assert.js';
import {BaseCompiler} from '../base-compiler.js';
import {PolkaVMAsmParser} from '../parsers/asm-parser-polkavm.js';
import {ResolcRiscVAsmParser} from '../parsers/asm-parser-resolc-riscv.js';
import {changeExtension} from '../utils.js';
import {type BaseParser, ResolcParser} from './argument-parsers.js';
/**
* The kind of input provided by the user.
* This is determined by the language chosen as Resolc
* supports both Solidity and Yul (Solidity IR).
*
* @note
* The enum value must exactly match the {@link Language.id}.
*/
enum InputKind {
Solidity = 'solidity',
Yul = 'yul',
}
/**
* The kind of output requested by the user.
*/
enum OutputKind {
PolkaVM = 'pvm',
RiscV = 'risc-v',
}
export class ResolcCompiler extends BaseCompiler {
static get key() {
return 'resolc';
}
private readonly pvmAsmParser: PolkaVMAsmParser;
/**
* @note
* Needs to coincide with the [infrastructure configs](https://github.com/compiler-explorer/infra/blob/main/bin/yaml/solidity.yaml).
*/
static get solcExe() {
return '/opt/compiler-explorer/solc-0.8.30/solc';
}
constructor(...args: ConstructorParameters<typeof BaseCompiler>) {
super(...args);
this.asm = new ResolcRiscVAsmParser(this.compilerProps);
this.pvmAsmParser = new PolkaVMAsmParser();
// The arg producing LLVM IR (among other output) is already
// included in optionsForFilter(), but irArg needs to be set.
this.compiler.irArg = [];
this.compiler.supportsIrView = true;
this.compiler.supportsIrViewOptToggleOption = true;
}
override getSharedLibraryPathsAsArguments(): string[] {
return [];
}
override getArgumentParserClass(): typeof BaseParser {
return ResolcParser;
}
override optionsForFilter(filters: ParseFiltersAndOutputOptions): string[] {
filters.binaryObject = this.reinterpretBinaryObjectFilter(filters.binaryObject);
filters.intel = false;
const options = ['-g', '--solc', ResolcCompiler.solcExe, '--overwrite', '--debug-output-dir', 'artifacts'];
if (this.inputIs(InputKind.Yul)) {
options.push('--yul');
}
return options;
}
override isCfgCompiler(): boolean {
return false;
}
override getOutputFilename(dirPath: string): string {
return this.getOutputFilenameWithExtension(dirPath, '.pvmasm');
}
override getIrOutputFilename(
inputFilename: string,
_filters?: ParseFiltersAndOutputOptions,
irOptions?: LLVMIrBackendOptions,
): string {
const extension =
irOptions?.showOptimized && this.compiler.supportsIrViewOptToggleOption
? '.optimized.ll'
: '.unoptimized.ll';
return this.getOutputFilenameWithExtension(path.dirname(inputFilename), extension);
}
override getObjdumpOutputFilename(defaultOutputFilename: string): string {
return changeExtension(defaultOutputFilename, '.o');
}
private getOutputFilenameWithExtension(dirPath: string, extension: string): string {
const basenamePrefix = dirPath.split(path.sep).join('_');
const contractName = this.inputIs(InputKind.Solidity)
? this.getSolidityContractName(dirPath)
: this.getYulContractName(dirPath);
const basename = `${basenamePrefix}_${this.compileFilename}.${contractName}${extension}`;
return path.join(dirPath, 'artifacts', basename);
}
override async processAsm(
result: CompilationResult,
filters: ParseFiltersAndOutputOptions,
): Promise<ParsedAsmResult> {
return this.outputIs(OutputKind.PolkaVM, filters)
? this.pvmAsmParser.process(result.asm as string, filters)
: this.asm.process(result.asm as string, filters);
}
override async postProcessAsm(
result: ParsedAsmResult,
filters?: ParseFiltersAndOutputOptions,
): Promise<ParsedAsmResult> {
result = await super.postProcessAsm(result, filters);
result = this.removeOrphanedLabels(result, filters);
this.maybeRemoveSourceMappings(result, filters);
this.addOutputHeader(result, filters);
return result;
}
/**
* Remove orphaned labels.
*
* @example
* Before:
* ```
* memset:
* .LBB35_2:
* .LBB35_3:
* __entry:
* addi sp, sp, -0x10
* ```
*
* After:
* ```
* __entry:
* addi sp, sp, -0x10
* ```
*/
private removeOrphanedLabels(result: ParsedAsmResult, filters?: ParseFiltersAndOutputOptions): ParsedAsmResult {
// Orphaned RISC-V labels may be produced by the AsmParser when library code is skipped.
if (!this.outputIs(OutputKind.RiscV, filters) || !filters?.libraryCode || !result.labelDefinitions) {
return result;
}
const {asm, labelDefinitions} = result;
result.asm = asm.filter((currentLine, index) => {
const nextLine = asm[index + 1];
const currentIsLabel = this.isLabel(currentLine, labelDefinitions);
const nextIsLabel = nextLine && this.isLabel(nextLine, labelDefinitions);
const isOrphaned = currentIsLabel && (nextIsLabel || !nextLine);
return !isOrphaned;
});
return result;
}
/**
* Current source mappings from RISC-V only map to the Yul line numbers. When
* a Solidity source file is used, the mappings shown in CE are thus misleading.
*/
private maybeRemoveSourceMappings(result: ParsedAsmResult, filters?: ParseFiltersAndOutputOptions): void {
const inputIsSolidity = this.inputIs(InputKind.Solidity);
const {asm, labelDefinitions} = result;
if (this.outputIs(OutputKind.RiscV, filters)) {
for (const line of asm) {
if (inputIsSolidity) {
line.source = null;
}
if (!this.isLabel(line, labelDefinitions)) {
line.text = ' ' + line.text;
}
}
}
}
/**
* Whether the parsed asm result line represents a label.
*/
private isLabel(line: ParsedAsmResultLine, labelDefinitions: ParsedAsmResult['labelDefinitions']): boolean {
return line.text.endsWith(':') && !!labelDefinitions && line.text.slice(0, -1) in labelDefinitions;
}
/**
* Whether the provided input kind matches the language used.
*/
private inputIs(kind: InputKind): boolean {
return this.lang.id === kind.valueOf();
}
/**
* Whether the provided output kind matches the output requested.
*/
private outputIs(kind: OutputKind, filters?: ParseFiltersAndOutputOptions): boolean {
switch (kind) {
case OutputKind.PolkaVM:
return !filters?.binaryObject;
case OutputKind.RiscV:
return !!filters?.binaryObject;
default:
throw new Error('Unexpected output kind.');
}
}
/**
* Reinterpret the user-provided binary object filter to show the PolkaVM
* assembly if selected, otherwise the RISC-V assembly.
*
* Users who select "Compile to binary object" should see the disassembled
* PVM plob and not RISC-V. However, to see the RISC-V output, the binary
* object filter needs to be reset to `true` in order to pass the binary
* object (which will already exist after the first compilation) to the
* objdumper during post-processing of the compilation result.
*/
private reinterpretBinaryObjectFilter(binaryObjectFilter?: boolean): boolean {
return !binaryObjectFilter;
}
/**
* Get the Solidity contract name used in the compile file.
*
* @example
* ```solidity
* contract Square { ... } // Name = Square
* ```
*/
private getSolidityContractName(dirPath: string): string {
const nameRe = /contract[\s\n]+(?<name>[\w$]+)[\s\n]*{/;
return this.getContractName(dirPath, nameRe);
}
/**
* Get the Yul contract name used in the compile file.
*
* @example
* ```
* object "Square" { ... } // Name = Square
* ```
*/
private getYulContractName(dirPath: string): string {
const nameRe = /object[\s\n]+"(?<name>[\w$.]+)"[\s\n]*{/;
return this.getContractName(dirPath, nameRe);
}
private getContractName(dirPath: string, nameRe: RegExp): string {
const source = fs.readFileSync(path.join(dirPath, this.compileFilename), {encoding: 'utf8'});
const match = source.match(nameRe);
assert(match?.groups?.name, 'Expected to find a contract name in the source file.');
return match.groups.name;
}
private addOutputHeader(result: ParsedAsmResult, filters?: ParseFiltersAndOutputOptions): void {
const pvmHeader =
'// PolkaVM Assembly:\n' +
'// --------------------------\n' +
'// To see the RISC-V assembly instead,\n' +
'// disable "Compile to binary object".\n' +
'// --------------------------';
const riscvHeader =
'; RISC-V (64 bits) Assembly:\n' +
'; --------------------------\n' +
'; To see the PolkaVM assembly instead,\n' +
'; enable "Compile to binary object".\n' +
'; --------------------------';
const header = this.outputIs(OutputKind.PolkaVM, filters) ? pvmHeader : riscvHeader;
result.asm.unshift(...header.split('\n').map(line => ({text: line})));
}
}

View File

@@ -972,6 +972,17 @@ const definitions: Record<LanguageKey, LanguageDefinition> = {
previewFilter: null,
monacoDisassembly: null,
},
yul: {
name: 'Yul (Solidity IR)',
monaco: 'yul',
extensions: ['.yul'],
alias: [],
logoFilename: 'solidity.svg',
logoFilenameDark: null,
formatter: null,
previewFilter: null,
monacoDisassembly: null,
},
zig: {
name: 'Zig',
monaco: 'zig',

View File

@@ -0,0 +1,177 @@
// 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 {ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
import {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
import {assert} from '../assert.js';
import {deltaTimeNanoToMili, splitLines} from '../utils.js';
import {IAsmParser} from './asm-parser.interfaces.js';
import {AsmRegex} from './asmregex.js';
/**
* The parser for PolkaVM assembly.
*
* @note
* There are currently no source mappings from PolkaVM.
*/
export class PolkaVMAsmParser implements IAsmParser {
/**
* @example ` 64: ra = 6, jump @42`
*/
protected instructionRe = /^\s+(?<address>\d+):\s+(?<disasm>.*)$/;
/**
* @example `<deploy>:`
*/
protected labelRe = /^<(?<label>[^\s\n]+)>:$/;
/**
* @example `// Stack size = 32768 bytes`
*/
protected headerCommentRe = /^\/\//;
/**
* @example ` // This is a comment`
*/
protected commentOnlyRe = /^\s*\/\//;
/**
* @example ` : @16 (gas: 5)`
*/
protected jumpTargetRe = /^\s*: (?<targetLine>@\d+.*)$/;
protected maxAsmLines = 5000;
protected indentation = ' ';
process(asmResult: string, filters: ParseFiltersAndOutputOptions): ParsedAsmResult {
const startTime = process.hrtime.bigint();
let asmLines = splitLines(asmResult);
const originalLineCount = asmLines.length;
if (filters.preProcessLines) {
asmLines = filters.preProcessLines(asmLines);
}
const {asm, labelDefinitions} = this.processLines(asmLines, filters);
const endTime = process.hrtime.bigint();
return {
asm,
labelDefinitions,
parsingTime: deltaTimeNanoToMili(startTime, endTime),
filteredCount: originalLineCount - asm.length,
};
}
processLines(
asmLines: string[],
filters: ParseFiltersAndOutputOptions,
): {
asm: ParsedAsmResultLine[];
labelDefinitions: Record<string, number>;
} {
const parsedAsm: ParsedAsmResultLine[] = [];
const labelDefinitions: Record<string, number> = {};
for (const line of asmLines) {
if (parsedAsm.length >= this.maxAsmLines) {
parsedAsm.push({
text: '[truncated; too many lines]',
source: null,
});
break;
}
let match: RegExpMatchArray | null = null;
if ((match = line.match(this.instructionRe))) {
this.parseInstruction(parsedAsm, match, filters);
} else if ((match = line.match(this.jumpTargetRe))) {
this.parseJumpTarget(parsedAsm, match, labelDefinitions);
} else if ((match = line.match(this.labelRe))) {
this.parseLabel(parsedAsm, match, labelDefinitions);
} else if ((match = line.match(this.commentOnlyRe))) {
this.parseComment(parsedAsm, match, line, filters);
}
}
return {asm: parsedAsm, labelDefinitions};
}
private parseInstruction(
parsedAsm: ParsedAsmResultLine[],
match: RegExpMatchArray,
filters: ParseFiltersAndOutputOptions,
): void {
assert(match.groups);
const address = parseInt(match.groups.address, 10);
const disassembly = this.indentation + AsmRegex.filterAsmLine(match.groups.disasm, filters);
parsedAsm.push({
address,
text: disassembly,
source: null,
});
}
private parseJumpTarget(
parsedAsm: ParsedAsmResultLine[],
match: RegExpMatchArray,
filters: ParseFiltersAndOutputOptions,
): void {
assert(match.groups);
const targetLine = this.indentation + AsmRegex.filterAsmLine(match.groups.targetLine, filters);
parsedAsm.push({
text: targetLine,
source: null,
});
}
private parseLabel(
parsedAsm: ParsedAsmResultLine[],
match: RegExpMatchArray,
labelDefinitions: Record<string, number>,
): void {
assert(match.groups);
const label = match.groups.label;
parsedAsm.push({
text: label + ':',
source: null,
});
labelDefinitions[label] = parsedAsm.length;
}
private parseComment(
parsedAsm: ParsedAsmResultLine[],
match: RegExpMatchArray,
line: string,
filters: ParseFiltersAndOutputOptions,
): void {
if (!filters.commentOnly) {
const comment = line.match(this.headerCommentRe)
? match.input?.trim()!
: this.indentation + match.input?.trim()!;
parsedAsm.push({
text: comment,
source: null,
});
}
}
}

View File

@@ -0,0 +1,35 @@
// 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 {PropertyGetter} from '../properties.interfaces.js';
import {AsmParser} from './asm-parser.js';
export class ResolcRiscVAsmParser extends AsmParser {
constructor(compilerProps?: PropertyGetter) {
super(compilerProps);
// Example: "; artifacts/_var_folders_fj_1p_1d_T_compiler-explorer-compilerJzYPPi_example.yul.Square.yul:1"
this.lineRe = /^;\s+(?<file>\S+):(?<line>\d+)$/;
}
}

View File

@@ -72,4 +72,5 @@ import './tablegen-mode';
import './v-mode';
import './vala-mode';
import './wat-mode';
import './yul-mode';
import './zig-mode';

161
static/modes/yul-mode.ts Normal file
View File

@@ -0,0 +1,161 @@
// 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';
export function definition(): monaco.languages.IMonarchLanguage {
return {
// Set defaultToken to 'invalid' to see what is not yet tokenized.
defaultToken: 'invalid',
keywords: [
'break',
'case',
'code',
'continue',
'data',
'default',
'false',
'for',
'function',
'hex',
'if',
'leave',
'let',
'object',
'switch',
'true',
],
operators: [':='],
brackets: [
{open: '{', close: '}', token: 'delimiter.curly'},
{open: '(', close: ')', token: 'delimiter.parenthesis'},
{open: '[', close: ']', token: 'delimiter.square'},
],
symbols: /[:=]+/,
escapes: /\\(?:['"\\nrt\n\r]|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})/,
tokenizer: {
root: [
// Identifiers and keywords
[
/[a-z][a-zA-Z0-9_]*/,
{
cases: {
'@keywords': 'keyword',
'@default': 'identifier',
},
},
],
[/[a-zA-Z_$][a-zA-Z_$0-9.]*/, 'type.identifier'],
// Whitespace
{include: '@whitespace'},
// Delimiters and operators
[/[()[\]{}]/, '@brackets'],
[/[.,:]/, 'delimiter'],
[
/@symbols/,
{
cases: {
'@operators': 'operator',
'@default': '',
},
},
],
// Numbers
[/0x[0-9a-fA-F]+/, 'number.hex'],
[/\d+/, 'number'],
// Strings
[/"([^"\\]|\\.)*$/, 'string.invalid'], // non-terminated string
[/"/, 'string', '@string'],
],
// Whitespace and comments
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/\/\*/, 'comment', '@comment'],
[/\/\/.*$/, 'comment'],
],
comment: [
[/[^/*]+/, 'comment'],
[/\/\*/, 'comment', '@push'], // Nested comment
['\\*/', 'comment', '@pop'],
[/[/*]/, 'comment'],
],
// Strings
string: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, 'string', '@pop'],
],
},
};
}
function configuration(): monaco.languages.LanguageConfiguration {
return {
comments: {
lineComment: '//',
blockComment: ['/*', '*/'],
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{open: '{', close: '}'},
{open: '[', close: ']'},
{open: '(', close: ')'},
{open: '"', close: '"', notIn: ['string']},
{open: '/*', close: ' */', notIn: ['string']},
],
surroundingPairs: [
{open: '{', close: '}'},
{open: '[', close: ']'},
{open: '(', close: ')'},
{open: '"', close: '"'},
],
};
}
monaco.languages.register({id: 'yul'});
monaco.languages.setMonarchTokensProvider('yul', definition());
monaco.languages.setLanguageConfiguration('yul', configuration());

View File

@@ -33,4 +33,5 @@ export interface IrState {
'filter-instruction-metadata'?: boolean;
'filter-attributes'?: boolean;
'filter-comments'?: boolean;
'show-optimized'?: boolean;
}

View File

@@ -69,6 +69,7 @@ export class Ir extends MonacoPane<monaco.editor.IStandaloneCodeEditor, IrState>
filterComments: true,
noDiscardValueNames: true,
demangle: true,
showOptimized: true,
};
private cfgButton: JQuery;
private wrapButton: JQuery<HTMLElement>;
@@ -307,6 +308,7 @@ export class Ir extends MonacoPane<monaco.editor.IStandaloneCodeEditor, IrState>
filterComments: filters['filter-comments'],
noDiscardValueNames: options['-fno-discard-value-names'],
demangle: options['demangle-symbols'],
showOptimized: options['show-optimized'],
};
let changed = false;
for (const k in newOptions) {
@@ -346,6 +348,8 @@ export class Ir extends MonacoPane<monaco.editor.IStandaloneCodeEditor, IrState>
if (compiler && !compiler.supportsIrView) {
this.editor.setValue('<LLVM IR output is not supported for this compiler>');
}
this.options.enableToggle('show-optimized', !!compiler?.supportsIrViewOptToggleOption);
}
showIrResults(result: any): void {

View File

@@ -25,7 +25,11 @@
import {describe, expect, it} from 'vitest';
import {AsmParser} from '../lib/parsers/asm-parser.js';
import {MlirAsmParser} from '../lib/parsers/asm-parser-mlir.js';
import {PolkaVMAsmParser} from '../lib/parsers/asm-parser-polkavm.js';
import {PTXAsmParser} from '../lib/parsers/asm-parser-ptx.js';
import {ResolcRiscVAsmParser} from '../lib/parsers/asm-parser-resolc-riscv.js';
import type {ParsedAsmResult} from '../types/asmresult/asmresult.interfaces.js';
import type {ParseFiltersAndOutputOptions} from '../types/features/filters.interfaces.js';
describe('AsmParser tests', () => {
const parser = new AsmParser();
@@ -293,3 +297,191 @@ module {
});
});
});
describe('ResolcRiscVAsmParser tests', () => {
const parser = new ResolcRiscVAsmParser();
function expectParsedAsmResult(result: ParsedAsmResult, expected: ParsedAsmResult): void {
expect(result.labelDefinitions).toEqual(expected.labelDefinitions);
expect(result.asm.length).toEqual(expected.asm.length);
for (let i = 0; i < result.asm.length; i++) {
expect(result.asm[i]).toMatchObject(expected.asm[i]);
}
}
it.skipIf(process.platform === 'win32')('should identify RISC-V instruction info and source line numbers', () => {
const filters: Partial<ParseFiltersAndOutputOptions> = {binaryObject: true};
const riscv = `
000000000000027a <__entry>:
; __entry():
; path/to/example.sol.Square.yul:1
27a: 41 11 addi sp, sp, -0x10
000000000000028c <.Lpcrel_hi4>:
; .Lpcrel_hi4():
28c: 97 05 00 00 auipc a1, 0x0
; path/to/example.sol.Square.yul:1
2a8: 1d 71 addi sp, sp, -0x60
2aa: 86 ec sd ra, 0x58(sp)
; path/to/example.sol.Square.yul:7
2ca: e7 80 00 00 jalr ra <.Lpcrel_hi4+0x3a>`;
const expected: ParsedAsmResult = {
asm: [
{
text: '__entry:',
source: null,
},
{
text: ' addi sp, sp, -0x10',
address: 0x27a,
opcodes: ['41', '11'],
source: {
line: 1,
file: null,
},
},
{
text: '.Lpcrel_hi4:',
source: null,
},
{
text: ' auipc a1, 0x0',
address: 0x28c,
opcodes: ['97', '05', '00', '00'],
source: {
line: 1,
file: null,
},
},
{
text: ' addi sp, sp, -0x60',
address: 0x2a8,
opcodes: ['1d', '71'],
source: {
line: 1,
file: null,
},
},
{
text: ' sd ra, 0x58(sp)',
address: 0x2aa,
opcodes: ['86', 'ec'],
source: {
line: 1,
file: null,
},
},
{
text: ' jalr ra <.Lpcrel_hi4+0x3a>',
address: 0x2ca,
opcodes: ['e7', '80', '00', '00'],
source: {
line: 7,
file: null,
},
},
],
labelDefinitions: {
__entry: 1,
['.Lpcrel_hi4']: 3,
},
};
const result = parser.processAsm(riscv, filters);
expectParsedAsmResult(result, expected);
});
});
describe('PolkaVMAsmParser tests', () => {
const parser = new PolkaVMAsmParser();
function expectParsedAsmResult(result: ParsedAsmResult, expected: ParsedAsmResult): void {
expect(result.labelDefinitions).toEqual(expected.labelDefinitions);
expect(result.asm.length).toEqual(expected.asm.length);
for (let i = 0; i < result.asm.length; i++) {
expect(result.asm[i]).toMatchObject(expected.asm[i]);
}
}
// Note: We currently have no source mappings from PVM.
it('should identify PVM instruction info', () => {
const filters: Partial<ParseFiltersAndOutputOptions> = {
binaryObject: false,
commentOnly: false,
};
const pvm = `
// Code size = 1078 bytes
<__entry>:
: @0 (gas: 6)
0: sp = sp + 0xfffffffffffffff0
3: u64 [sp + 0x8] = ra
6: u64 [sp] = s0
8: s0 = a0 & 0x1
11: ecalli 2 // 'call_data_size'
13: fallthrough
: @1 (gas: 2)
14: u32 [0x20000] = a0`;
const expected: ParsedAsmResult = {
asm: [
{
text: '// Code size = 1078 bytes',
source: null,
},
{
text: '__entry:',
source: null,
},
{
text: ' @0 (gas: 6)',
source: null,
},
{
text: ' sp = sp + 0xfffffffffffffff0',
address: 0,
source: null,
},
{
text: ' u64 [sp + 0x8] = ra',
address: 3,
source: null,
},
{
text: ' u64 [sp] = s0',
address: 6,
source: null,
},
{
text: ' s0 = a0 & 0x1',
address: 8,
source: null,
},
{
text: " ecalli 2 // 'call_data_size'",
address: 11,
source: null,
},
{
text: ' fallthrough',
address: 13,
source: null,
},
{
text: ' @1 (gas: 2)',
source: null,
},
{
text: ' u32 [0x20000] = a0',
address: 14,
source: null,
},
],
labelDefinitions: {__entry: 2},
};
const result = parser.process(pvm, filters);
expectParsedAsmResult(result, expected);
});
});

307
test/resolc-tests.ts Normal file
View File

@@ -0,0 +1,307 @@
// 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 {beforeAll, describe, expect, it} from 'vitest';
import type {CompilationEnvironment} from '../lib/compilation-env.js';
import {ResolcParser} from '../lib/compilers/argument-parsers.js';
import {ResolcCompiler} from '../lib/compilers/index.js';
import type {ParsedAsmResult, ParsedAsmResultLine} from '../types/asmresult/asmresult.interfaces.js';
import type {CompilerInfo} from '../types/compiler.interfaces.js';
import type {ParseFiltersAndOutputOptions} from '../types/features/filters.interfaces.js';
import type {LanguageKey} from '../types/languages.interfaces.js';
import {makeCompilationEnvironment, makeFakeCompilerInfo, makeFakeLlvmIrBackendOptions} from './utils.js';
const languages = {
solidity: {id: 'solidity' as LanguageKey},
yul: {id: 'yul' as LanguageKey},
};
describe('Resolc', () => {
let env: CompilationEnvironment;
const expectedSolcExe = '/opt/compiler-explorer/solc-0.8.30/solc';
beforeAll(() => {
env = makeCompilationEnvironment({languages});
});
function makeCompiler(compilerInfo: Partial<CompilerInfo>): ResolcCompiler {
return new ResolcCompiler(makeFakeCompilerInfo(compilerInfo), env);
}
function expectCorrectOutputFilenames(
compiler: ResolcCompiler,
inputFilename: string,
expectedFilenameWithoutExtension: string,
): void {
const defaultOutputFilename = `${expectedFilenameWithoutExtension}.pvmasm`;
expect(compiler.getOutputFilename(path.normalize('test/resolc'))).toEqual(defaultOutputFilename);
let llvmIrBackendOptions = makeFakeLlvmIrBackendOptions({showOptimized: true});
expect(compiler.getIrOutputFilename(inputFilename, undefined, llvmIrBackendOptions)).toEqual(
`${expectedFilenameWithoutExtension}.optimized.ll`,
);
llvmIrBackendOptions = makeFakeLlvmIrBackendOptions({showOptimized: false});
expect(compiler.getIrOutputFilename(inputFilename, undefined, llvmIrBackendOptions)).toEqual(
`${expectedFilenameWithoutExtension}.unoptimized.ll`,
);
expect(compiler.getObjdumpOutputFilename(defaultOutputFilename)).toEqual(
`${expectedFilenameWithoutExtension}.o`,
);
}
describe('Common', () => {
it('should return correct key', () => {
expect(ResolcCompiler.key).toEqual('resolc');
});
it('should return Solc executable dependency path', () => {
expect(ResolcCompiler.solcExe).toEqual(expectedSolcExe);
});
});
describe('From Solidity', () => {
const compilerInfo = {
exe: 'resolc',
lang: languages.solidity.id,
};
it('should instantiate successfully', () => {
const compiler = makeCompiler(compilerInfo);
expect(compiler.lang.id).toEqual(compilerInfo.lang);
});
it('should use Resolc argument parser', () => {
const compiler = makeCompiler(compilerInfo);
expect(compiler.getArgumentParserClass()).toBe(ResolcParser);
});
it('should use debug options', () => {
const compiler = makeCompiler(compilerInfo);
expect(compiler.optionsForFilter({})).toEqual([
'-g',
'--solc',
expectedSolcExe,
'--overwrite',
'--debug-output-dir',
'artifacts',
]);
});
it('should generate output filenames', () => {
const compiler = makeCompiler(compilerInfo);
const filenameWithoutExtension = path.normalize('test/resolc/artifacts/test_resolc_example.sol.Square');
const inputFilename = path.normalize('test/resolc/example.sol');
expectCorrectOutputFilenames(compiler, inputFilename, filenameWithoutExtension);
});
describe('To RISC-V', () => {
const filters: Partial<ParseFiltersAndOutputOptions> = {
binaryObject: true,
libraryCode: true,
};
function getExpectedParsedOutputHeader(): ParsedAsmResultLine[] {
const header =
'; RISC-V (64 bits) Assembly:\n' +
'; --------------------------\n' +
'; To see the PolkaVM assembly instead,\n' +
'; enable "Compile to binary object".\n' +
'; --------------------------';
return header.split('\n').map(line => ({text: line}));
}
it('should remove orphaned labels', async () => {
const compiler = makeCompiler(compilerInfo);
const parsedAsm: ParsedAsmResult = {
asm: [
{
// Orphan
text: 'memmove:',
},
{
// Orphan
text: '.LBB34_5:',
},
{
// Orphan
text: 'memset:',
},
{
// Orphan
text: '.LBB35_2:',
},
{
text: '__entry:',
},
{
text: ' addi sp, sp, -0x10',
},
{
text: ' sd ra, 0x8(sp)',
},
{
// Orphan
text: '__last:',
},
],
labelDefinitions: {
memmove: 1,
['.LBB34_5']: 2,
memset: 3,
['.LBB35_2']: 4,
__entry: 5,
__last: 8,
},
};
const expected: ParsedAsmResult = {
asm: [
...getExpectedParsedOutputHeader(),
{
text: '__entry:',
},
{
text: ' addi sp, sp, -0x10',
},
{
text: ' sd ra, 0x8(sp)',
},
],
};
const result = await compiler.postProcessAsm(parsedAsm, filters);
expect(result.asm.length).toEqual(expected.asm.length);
expect(result.asm).toMatchObject(expected.asm);
});
it('should remove invalid Solidity <--> RISC-V source mappings', async () => {
const compiler = makeCompiler(compilerInfo);
const parsedAsm: ParsedAsmResult = {
asm: [
{
text: '.Lpcrel_hi4:',
source: null,
},
{
text: ' auipc a1, 0x0',
source: null,
},
{
text: ' addi sp, sp, -0x60',
source: {
line: 1,
file: null,
},
},
{
text: ' sd ra, 0x58(sp)',
source: {
line: 1,
file: null,
},
},
{
text: ' jalr ra <.Lpcrel_hi4+0x3a>',
source: {
line: 7,
file: null,
},
},
],
labelDefinitions: {['.Lpcrel_hi4']: 1},
};
const expected: ParsedAsmResult = {
asm: [
...getExpectedParsedOutputHeader(),
{
text: '.Lpcrel_hi4:',
source: null,
},
{
text: ' auipc a1, 0x0',
source: null,
},
{
text: ' addi sp, sp, -0x60',
source: null,
},
{
text: ' sd ra, 0x58(sp)',
source: null,
},
{
text: ' jalr ra <.Lpcrel_hi4+0x3a>',
source: null,
},
],
};
const result = await compiler.postProcessAsm(parsedAsm, filters);
expect(result.asm.length).toEqual(expected.asm.length);
expect(result.asm).toMatchObject(expected.asm);
});
});
});
describe('From Yul', () => {
const compilerInfo = {
exe: 'resolc',
lang: languages.yul.id,
};
it('should instantiate successfully', () => {
const compiler = makeCompiler(compilerInfo);
expect(compiler.lang.id).toEqual(compilerInfo.lang);
});
it('should use debug options', () => {
const compiler = makeCompiler(compilerInfo);
expect(compiler.optionsForFilter({})).toEqual([
'-g',
'--solc',
expectedSolcExe,
'--overwrite',
'--debug-output-dir',
'artifacts',
'--yul',
]);
});
it('should generate output filenames', () => {
const compiler = makeCompiler(compilerInfo);
const filenameWithoutExtension = path.normalize('test/resolc/artifacts/test_resolc_example.yul.Square');
const inputFilename = path.normalize('test/resolc/example.yul');
expectCorrectOutputFilenames(compiler, inputFilename, filenameWithoutExtension);
});
});
});

8
test/resolc/example.sol Normal file
View File

@@ -0,0 +1,8 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.4.0;
contract Square {
function square(uint32 num) public pure returns (uint32) {
return num * num;
}
}

43
test/resolc/example.yul Normal file
View File

@@ -0,0 +1,43 @@
object "Square" {
code {
{
let _1 := memoryguard(0x80)
mstore(64, _1)
if callvalue() { revert(0, 0) }
let _2 := datasize("Square_deployed")
codecopy(_1, dataoffset("Square_deployed"), _2)
return(_1, _2)
}
}
object "Square_deployed" {
code {
{
let _1 := memoryguard(0x80)
mstore(64, _1)
if iszero(lt(calldatasize(), 4))
{
if eq(0xd27b3841, shr(224, calldataload(0)))
{
if callvalue() { revert(0, 0) }
if slt(add(calldatasize(), not(3)), 32) { revert(0, 0) }
let value := calldataload(4)
let _2 := and(value, 0xffffffff)
if iszero(eq(value, _2)) { revert(0, 0) }
let product_raw := mul(_2, _2)
let product := and(product_raw, 0xffffffff)
if iszero(eq(product, product_raw))
{
mstore(0, shl(224, 0x4e487b71))
mstore(4, 0x11)
revert(0, 0x24)
}
mstore(_1, product)
return(_1, 32)
}
}
revert(0, 0)
}
}
data ".metadata" hex"a26469706673582212209b2b1b86ce0e1a75faa800884ba155bd6bc6a6bc71f210370f818535dcfc5ee364736f6c634300081e0033"
}
}

View File

@@ -40,6 +40,11 @@ import {AsmEWAVRParser} from '../lib/parsers/asm-parser-ewavr.js';
import {PTXAsmParser} from '../lib/parsers/asm-parser-ptx.js';
import {SassAsmParser} from '../lib/parsers/asm-parser-sass.js';
import {VcAsmParser} from '../lib/parsers/asm-parser-vc.js';
import {CompilerProps, fakeProps} from '../lib/properties.js';
import {LLVMIrBackendOptions} from '../types/compilation/ir.interfaces.js';
import {CompilerInfo} from '../types/compiler.interfaces.js';
import {ParseFiltersAndOutputOptions} from '../types/features/filters.interfaces.js';
import {Language} from '../types/languages.interfaces.js';
// Test helper class that extends AsmParser to allow setting protected properties for testing
class AsmParserForTest extends AsmParser {
@@ -48,11 +53,6 @@ class AsmParserForTest extends AsmParser {
}
}
import {CompilerProps, fakeProps} from '../lib/properties.js';
import {CompilerInfo} from '../types/compiler.interfaces.js';
import {ParseFiltersAndOutputOptions} from '../types/features/filters.interfaces.js';
import {Language} from '../types/languages.interfaces.js';
function ensureTempCleanup() {
// Sometimes we're called from inside a test, sometimes from outside. Handle both.
afterEach(async () => await temp.cleanup());
@@ -85,6 +85,10 @@ export function makeFakeParseFiltersAndOutputOptions(
return options as ParseFiltersAndOutputOptions;
}
export function makeFakeLlvmIrBackendOptions(options: Partial<LLVMIrBackendOptions>): LLVMIrBackendOptions {
return options as LLVMIrBackendOptions;
}
// This combines a should assert and a type guard
// Example:
//

View File

@@ -29,4 +29,5 @@ export type LLVMIrBackendOptions = {
filterComments: boolean;
noDiscardValueNames?: boolean;
demangle: boolean;
showOptimized?: boolean;
};

View File

@@ -82,6 +82,7 @@ export type CompilerInfo = {
supportsPpView?: boolean;
supportsAstView?: boolean;
supportsIrView?: boolean;
supportsIrViewOptToggleOption?: boolean;
supportsClangirView?: boolean;
supportsRustMirView?: boolean;
supportsRustMacroExpView?: boolean;

View File

@@ -108,8 +108,9 @@ export type LanguageKey =
| 'vb'
| 'vyper'
| 'wasm'
| 'zig'
| 'ylc';
| 'ylc'
| 'yul'
| 'zig';
export interface Language {
/** Id of language. Added programmatically based on CELanguages key */

View File

@@ -19,6 +19,7 @@ mixin optionButton(bind, isActive, text, title)
.dropdown-menu
+optionButton("demangle-symbols", true, "Demangle Symbols", "Demangle symbols")
+optionButton("-fno-discard-value-names", true, "-fno-discard-value-names", "Keep value names instead of LLVM value numbers")
+optionButton("show-optimized", true, "Show Optimized", "Show the optimized LLVM IR output")
.btn-group.btn-group-sm.filters(role="group")
button.btn.btn-sm.btn-light.dropdown-toggle(type="button" title="LLVM Opt Pass Filters" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Set output filters")
span.fas.fa-filter