From 5a15d893d707c75f42103383beb36d46148126fa Mon Sep 17 00:00:00 2001
From: LJ <81748770+elle-j@users.noreply.github.com>
Date: Tue, 4 Nov 2025 16:00:19 +0100
Subject: [PATCH] Add support for Yul intermediate view when compiling Solidity
(#8219)
## What
Adds support for seeing Yul (Solidity IR) as intermediate output when
compiling Solidity.
This PR also enables that view for the Resolc compiler.
### Main Additions
- [x] Support viewing Yul in a supplementary view
- Solidity compilers can enable this by setting
`this.compiler.supportsYulView = true` in the compiler's constructor
- If custom processing of the Yul output or the Yul output filename is
needed, the compiler can override `processYulOutput()` or
`getYulOutputFilename()`
- [x] Enable the Yul view for Resolc
- [x] Implement a Yul backend option for filtering out debug info from
the output
### Notes
Source mappings are currently not handled for Yul -> Solidity.
## Overall Usage
### Steps
* Choose Solidity as the language
* Choose a Resolc compiler
* View intermediate results:
* Yul
* (Hide/show debug info by toggling "Hide Debug Info" in the Yul view
filters)
## Screenshots
---
cypress/e2e/frontend.cy.ts | 2 +
lib/base-compiler.ts | 38 +++++
lib/compilers/resolc.ts | 1 +
static/components.interfaces.ts | 11 ++
static/components.ts | 34 ++++
static/event-map.ts | 4 +
static/hub.ts | 7 +
static/panes/compiler.ts | 58 +++++++
static/panes/diff.interfaces.ts | 1 +
static/panes/diff.ts | 8 +
static/panes/yul-view.interfaces.ts | 30 ++++
static/panes/yul-view.ts | 175 ++++++++++++++++++++
test/resolc-tests.ts | 2 +-
types/compilation/compilation.interfaces.ts | 4 +
types/compilation/yul.interfaces.ts | 27 +++
types/compiler.interfaces.ts | 1 +
views/templates/panes/compiler.pug | 1 +
views/templates/panes/yul.pug | 17 ++
views/templates/templates.pug | 4 +
19 files changed, 424 insertions(+), 1 deletion(-)
create mode 100644 static/panes/yul-view.interfaces.ts
create mode 100644 static/panes/yul-view.ts
create mode 100644 types/compilation/yul.interfaces.ts
create mode 100644 views/templates/panes/yul.pug
diff --git a/cypress/e2e/frontend.cy.ts b/cypress/e2e/frontend.cy.ts
index c8f80dfbb..c4d368884 100644
--- a/cypress/e2e/frontend.cy.ts
+++ b/cypress/e2e/frontend.cy.ts
@@ -15,6 +15,7 @@ const PANE_DATA_MAP = {
core: {name: 'Core', selector: 'view-haskellCore'},
stg: {name: 'STG', selector: 'view-haskellStg'},
cmm: {name: 'Cmm', selector: 'view-haskellCmm'},
+ yul: {name: 'Yul', selector: 'view-yul'},
// TODO find a way to properly hack the state URL to test this pane like the rust
// ones seem to be able to do.
// clojure_macro: {name: 'Clojure Macro', selector: 'view-clojuremacroexp'},
@@ -68,6 +69,7 @@ describe('Individual pane testing', () => {
addPaneOpenTest(PANE_DATA_MAP.core);
addPaneOpenTest(PANE_DATA_MAP.stg);
addPaneOpenTest(PANE_DATA_MAP.cmm);
+ addPaneOpenTest(PANE_DATA_MAP.yul);
addPaneOpenTest(PANE_DATA_MAP.dump);
addPaneOpenTest(PANE_DATA_MAP.tree);
addPaneOpenTest(PANE_DATA_MAP.debug);
diff --git a/lib/base-compiler.ts b/lib/base-compiler.ts
index 9a49130e4..a8becdec7 100644
--- a/lib/base-compiler.ts
+++ b/lib/base-compiler.ts
@@ -66,6 +66,7 @@ import type {
OptPipelineBackendOptions,
OptPipelineOutput,
} from '../types/compilation/opt-pipeline-output.interfaces.js';
+import type {YulBackendOptions} from '../types/compilation/yul.interfaces.js';
import type {CompilerInfo, PreliminaryCompilerInfo} from '../types/compiler.interfaces.js';
import {
BasicExecutionResult,
@@ -1628,6 +1629,10 @@ export class BaseCompiler {
return utils.changeExtension(inputFilename, '.dump-cmm');
}
+ getYulOutputFilename(defaultOutputFilename: string) {
+ return utils.changeExtension(defaultOutputFilename, '.yul');
+ }
+
// Currently called for getting macro expansion and HIR.
// It returns the content of the output file created after using -Z unpretty=.
// The outputFriendlyName is a free form string used in case of error.
@@ -1706,6 +1711,32 @@ export class BaseCompiler {
return [{text: 'Internal error; unable to open output path'}];
}
+ async processYulOutput(
+ defaultOutputFilename: string,
+ result: CompilationResult,
+ yulOptions: YulBackendOptions,
+ ): Promise {
+ if (result.code !== 0) {
+ return [{text: 'Failed to run compiler to get Yul intermediary output'}];
+ }
+
+ const outputFilename = this.getYulOutputFilename(defaultOutputFilename);
+ if (await utils.fileExists(outputFilename)) {
+ const content = await fs.readFile(outputFilename, 'utf8');
+ const result: ResultLine[] = content.split('\n').map(line => ({text: line}));
+ const filters: RegExp[] = [];
+
+ if (yulOptions.filterDebugInfo) {
+ const debugInfoRe = /^\s*\/\/\/ @(use-src|src|ast-id)/;
+ filters.push(debugInfoRe);
+ }
+
+ return result.filter(line => filters.every(re => !line.text.match(re)));
+ }
+
+ return [{text: 'Internal error: Unable to open output path'}];
+ }
+
/**
* Get the LLVM IR output filename.
*
@@ -2447,6 +2478,7 @@ export class BaseCompiler {
const makeHaskellStg = backendOptions.produceHaskellStg && this.compiler.supportsHaskellStgView;
const makeHaskellCmm = backendOptions.produceHaskellCmm && this.compiler.supportsHaskellCmmView;
const makeGccDump = backendOptions.produceGccDump?.opened && this.compiler.supportsGccDump;
+ const makeYul = backendOptions.produceYul && this.compiler.supportsYulView;
const [
asmResult,
@@ -2513,6 +2545,10 @@ export class BaseCompiler {
? await this.processHaskellExtraOutput(this.getHaskellCmmOutputFilename(inputFilename), asmResult)
: undefined;
+ const yulResult = makeYul
+ ? await this.processYulOutput(outputFilename, asmResult, backendOptions.produceYul)
+ : undefined;
+
asmResult.dirPath = dirPath;
if (!asmResult.compilationOptions) asmResult.compilationOptions = options;
asmResult.downloads = downloads;
@@ -2565,6 +2601,8 @@ export class BaseCompiler {
asmResult.clojureMacroExpOutput = clojureMacroExpResult;
+ asmResult.yulOutput = yulResult;
+
if (asmResult.code !== 0) {
return [{...asmResult, asm: ''}, [], []];
}
diff --git a/lib/compilers/resolc.ts b/lib/compilers/resolc.ts
index 01bdc6768..d3f01f872 100644
--- a/lib/compilers/resolc.ts
+++ b/lib/compilers/resolc.ts
@@ -85,6 +85,7 @@ export class ResolcCompiler extends BaseCompiler {
this.compiler.irArg = [];
this.compiler.supportsIrView = true;
this.compiler.supportsIrViewOptToggleOption = true;
+ this.compiler.supportsYulView = this.inputIs(InputKind.Solidity);
}
override getSharedLibraryPathsAsArguments(): string[] {
diff --git a/static/components.interfaces.ts b/static/components.interfaces.ts
index 10741cd9f..6e01d23e1 100644
--- a/static/components.interfaces.ts
+++ b/static/components.interfaces.ts
@@ -78,6 +78,7 @@ export const RUST_HIR_VIEW_COMPONENT_NAME = 'rusthir' as const;
export const CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME = 'clojuremacroexp' as const;
export const DEVICE_VIEW_COMPONENT_NAME = 'device' as const;
export const EXPLAIN_VIEW_COMPONENT_NAME = 'explain' as const;
+export const YUL_VIEW_COMPONENT_NAME = 'yul' as const;
export type StateWithLanguage = {lang: string};
// TODO(#7808): Normalize state types to reduce duplication (see #4490)
@@ -356,6 +357,15 @@ export type PopulatedExplainViewState = StateWithId & {
treeid: number;
};
+export type EmptyYulViewState = EmptyState;
+export type PopulatedYulViewState = StateWithId & {
+ source: string;
+ yulOutput: unknown;
+ compilerName: string;
+ editorid: number;
+ treeid: number;
+};
+
/**
* Mapping of component names to their expected state types. This provides compile-time type safety for component
* states. Components can have either empty (default) or populated states.
@@ -392,6 +402,7 @@ export interface ComponentStateMap {
[CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME]: EmptyClojureMacroExpViewState | PopulatedClojureMacroExpViewState;
[DEVICE_VIEW_COMPONENT_NAME]: EmptyDeviceViewState | PopulatedDeviceViewState;
[EXPLAIN_VIEW_COMPONENT_NAME]: EmptyExplainViewState | PopulatedExplainViewState;
+ [YUL_VIEW_COMPONENT_NAME]: EmptyYulViewState | PopulatedYulViewState;
}
/**
diff --git a/static/components.ts b/static/components.ts
index 8443ded7e..b91991e24 100644
--- a/static/components.ts
+++ b/static/components.ts
@@ -66,6 +66,7 @@ import {
TOOL_COMPONENT_NAME,
TOOL_INPUT_VIEW_COMPONENT_NAME,
TREE_COMPONENT_NAME,
+ YUL_VIEW_COMPONENT_NAME,
} from './components.interfaces.js';
import {GccDumpViewState} from './panes/gccdump-view.interfaces.js';
import {SentryCapture} from './sentry.js';
@@ -782,6 +783,38 @@ export function getHaskellCmmViewWith(
};
}
+/** Get an empty Yul view component. */
+export function getYulView(): ComponentConfig {
+ return {
+ type: 'component',
+ componentName: YUL_VIEW_COMPONENT_NAME,
+ componentState: {},
+ };
+}
+
+/** Get a Yul view with the given configuration. */
+export function getYulViewWith(
+ id: number,
+ source: string,
+ yulOutput: unknown,
+ compilerName: string,
+ editorid: number,
+ treeid: number,
+): ComponentConfig {
+ return {
+ type: 'component',
+ componentName: YUL_VIEW_COMPONENT_NAME,
+ componentState: {
+ id,
+ source,
+ yulOutput,
+ compilerName,
+ editorid,
+ treeid,
+ },
+ };
+}
+
/** Get an empty gnat debug tree view component. */
export function getGnatDebugTreeView(): ComponentConfig {
return {
@@ -1233,6 +1266,7 @@ function validateComponentState(componentName: string, state: any): boolean {
case RUST_HIR_VIEW_COMPONENT_NAME:
case CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME:
case DEVICE_VIEW_COMPONENT_NAME:
+ case YUL_VIEW_COMPONENT_NAME:
return true;
default:
diff --git a/static/event-map.ts b/static/event-map.ts
index 824d5e606..2aa162562 100644
--- a/static/event-map.ts
+++ b/static/event-map.ts
@@ -26,6 +26,7 @@ import {ClangirBackendOptions} from '../types/compilation/clangir.interfaces.js'
import {CompilationResult} from '../types/compilation/compilation.interfaces.js';
import {LLVMIrBackendOptions} from '../types/compilation/ir.interfaces.js';
import {OptPipelineBackendOptions} from '../types/compilation/opt-pipeline-output.interfaces.js';
+import {YulBackendOptions} from '../types/compilation/yul.interfaces.js';
import {CompilerInfo} from '../types/compiler.interfaces.js';
import {Language, LanguageKey} from '../types/languages.interfaces.js';
import {MessageWithLocation} from '../types/resultline/resultline.interfaces.js';
@@ -160,6 +161,9 @@ export type EventMap = {
rustMirViewOpened: (compilerId: number) => void;
clojureMacroExpViewClosed: (compilerId: number) => void;
clojureMacroExpViewOpened: (compilerId: number) => void;
+ yulViewClosed: (compilerId: number) => void;
+ yulViewOpened: (compilerId: number) => void;
+ yulViewOptionsUpdated: (compilerId: number, options: YulBackendOptions, recompile: boolean) => void;
// TODO: There are no emitters for this event
selectLine: (editorId: number, lineNumber: number) => void;
settingsChange: (newSettings: SiteSettings) => void;
diff --git a/static/hub.ts b/static/hub.ts
index 7485e47ab..af0a88723 100644
--- a/static/hub.ts
+++ b/static/hub.ts
@@ -59,6 +59,7 @@ import {
TOOL_COMPONENT_NAME,
TOOL_INPUT_VIEW_COMPONENT_NAME,
TREE_COMPONENT_NAME,
+ YUL_VIEW_COMPONENT_NAME,
} from './components.interfaces.js';
import {EventHub} from './event-hub.js';
import {EventMap} from './event-map.js';
@@ -93,6 +94,7 @@ import {StackUsage as StackUsageView} from './panes/stack-usage-view.js';
import {Tool} from './panes/tool.js';
import {ToolInputView} from './panes/tool-input-view.js';
import {Tree, TreeState} from './panes/tree.js';
+import {Yul as YulView} from './panes/yul-view.js';
type GLC = GoldenLayout.Container;
@@ -173,6 +175,7 @@ export class Hub {
this.conformanceViewFactory(c, s),
);
layout.registerComponent(EXPLAIN_VIEW_COMPONENT_NAME, (c: GLC, s: any) => this.explainViewFactory(c, s));
+ layout.registerComponent(YUL_VIEW_COMPONENT_NAME, (c: GLC, s: any) => this.yulViewFactory(c, s));
layout.eventHub.on(
'editorOpen',
@@ -604,4 +607,8 @@ export class Hub {
public explainViewFactory(container: GoldenLayout.Container, state: any): ExplainView {
return new ExplainView(this, container, state);
}
+
+ public yulViewFactory(container: GoldenLayout.Container, state: InferComponentState): YulView {
+ return new YulView(this, container, state);
+ }
}
diff --git a/static/panes/compiler.ts b/static/panes/compiler.ts
index 9d09b09c3..53d020fdf 100644
--- a/static/panes/compiler.ts
+++ b/static/panes/compiler.ts
@@ -75,6 +75,7 @@ import fileSaver from 'file-saver';
import {escapeHTML, splitArguments} from '../../shared/common-utils.js';
import {ClangirBackendOptions} from '../../types/compilation/clangir.interfaces.js';
import {LLVMIrBackendOptions} from '../../types/compilation/ir.interfaces.js';
+import {YulBackendOptions} from '../../types/compilation/yul.interfaces.js';
import {CompilerOutputOptions} from '../../types/features/filters.interfaces.js';
import {InstructionSet} from '../../types/instructionsets.js';
import {LanguageKey} from '../../types/languages.interfaces.js';
@@ -194,6 +195,7 @@ export class Compiler extends MonacoPane;
private haskellCmmButton: JQuery;
private clojureMacroExpButton: JQuery;
+ private yulButton: JQuery;
private gccDumpButton: JQuery;
private cfgButton: JQuery;
private explainButton: JQuery;
@@ -271,9 +273,11 @@ export class Compiler extends MonacoPane void) & _.Cancelable;
@@ -636,6 +640,17 @@ export class Compiler extends MonacoPane {
+ return Components.getYulViewWith(
+ this.id,
+ this.source,
+ this.lastResult?.yulOutput,
+ this.getCompilerName(),
+ this.sourceEditorId ?? 0,
+ this.sourceTreeId ?? 0,
+ );
+ };
+
const createGccDumpView = () => {
return Components.getGccDumpViewWith(
this.id,
@@ -920,6 +935,18 @@ export class Compiler extends MonacoPane createYulView()).on(
+ 'dragStart',
+ hidePaneAdder,
+ );
+
+ this.yulButton.on('click', () => {
+ const insertPoint =
+ this.hub.findParentRowOrColumn(this.container.parent) ||
+ this.container.layoutManager.root.contentItems[0];
+ insertPoint.addChild(createYulView());
+ });
+
createDragSource(this.container.layoutManager, this.gccDumpButton, () => createGccDumpView()).on(
'dragStart',
hidePaneAdder,
@@ -1316,6 +1343,7 @@ export class Compiler extends MonacoPane"},
];
break;
+ case DiffType.YulOutput:
+ output = this.result.yulOutput || [
+ {text: "