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

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:
//