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

<img width="1502" height="903" alt="ce-yul-view"
src="https://github.com/user-attachments/assets/ccc897e2-cd8d-4c33-962c-522d60b63134"
/>
This commit is contained in:
LJ
2025-11-04 16:00:19 +01:00
committed by GitHub
parent 9b76c60701
commit 5a15d893d7
19 changed files with 424 additions and 1 deletions

View File

@@ -15,6 +15,7 @@ const PANE_DATA_MAP = {
core: {name: 'Core', selector: 'view-haskellCore'}, core: {name: 'Core', selector: 'view-haskellCore'},
stg: {name: 'STG', selector: 'view-haskellStg'}, stg: {name: 'STG', selector: 'view-haskellStg'},
cmm: {name: 'Cmm', selector: 'view-haskellCmm'}, 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 // TODO find a way to properly hack the state URL to test this pane like the rust
// ones seem to be able to do. // ones seem to be able to do.
// clojure_macro: {name: 'Clojure Macro', selector: 'view-clojuremacroexp'}, // 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.core);
addPaneOpenTest(PANE_DATA_MAP.stg); addPaneOpenTest(PANE_DATA_MAP.stg);
addPaneOpenTest(PANE_DATA_MAP.cmm); addPaneOpenTest(PANE_DATA_MAP.cmm);
addPaneOpenTest(PANE_DATA_MAP.yul);
addPaneOpenTest(PANE_DATA_MAP.dump); addPaneOpenTest(PANE_DATA_MAP.dump);
addPaneOpenTest(PANE_DATA_MAP.tree); addPaneOpenTest(PANE_DATA_MAP.tree);
addPaneOpenTest(PANE_DATA_MAP.debug); addPaneOpenTest(PANE_DATA_MAP.debug);

View File

@@ -66,6 +66,7 @@ import type {
OptPipelineBackendOptions, OptPipelineBackendOptions,
OptPipelineOutput, OptPipelineOutput,
} from '../types/compilation/opt-pipeline-output.interfaces.js'; } 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 type {CompilerInfo, PreliminaryCompilerInfo} from '../types/compiler.interfaces.js';
import { import {
BasicExecutionResult, BasicExecutionResult,
@@ -1628,6 +1629,10 @@ export class BaseCompiler {
return utils.changeExtension(inputFilename, '.dump-cmm'); return utils.changeExtension(inputFilename, '.dump-cmm');
} }
getYulOutputFilename(defaultOutputFilename: string) {
return utils.changeExtension(defaultOutputFilename, '.yul');
}
// Currently called for getting macro expansion and HIR. // Currently called for getting macro expansion and HIR.
// It returns the content of the output file created after using -Z unpretty=<unprettyOpt>. // It returns the content of the output file created after using -Z unpretty=<unprettyOpt>.
// The outputFriendlyName is a free form string used in case of error. // 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'}]; return [{text: 'Internal error; unable to open output path'}];
} }
async processYulOutput(
defaultOutputFilename: string,
result: CompilationResult,
yulOptions: YulBackendOptions,
): Promise<ResultLine[]> {
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. * Get the LLVM IR output filename.
* *
@@ -2447,6 +2478,7 @@ export class BaseCompiler {
const makeHaskellStg = backendOptions.produceHaskellStg && this.compiler.supportsHaskellStgView; const makeHaskellStg = backendOptions.produceHaskellStg && this.compiler.supportsHaskellStgView;
const makeHaskellCmm = backendOptions.produceHaskellCmm && this.compiler.supportsHaskellCmmView; const makeHaskellCmm = backendOptions.produceHaskellCmm && this.compiler.supportsHaskellCmmView;
const makeGccDump = backendOptions.produceGccDump?.opened && this.compiler.supportsGccDump; const makeGccDump = backendOptions.produceGccDump?.opened && this.compiler.supportsGccDump;
const makeYul = backendOptions.produceYul && this.compiler.supportsYulView;
const [ const [
asmResult, asmResult,
@@ -2513,6 +2545,10 @@ export class BaseCompiler {
? await this.processHaskellExtraOutput(this.getHaskellCmmOutputFilename(inputFilename), asmResult) ? await this.processHaskellExtraOutput(this.getHaskellCmmOutputFilename(inputFilename), asmResult)
: undefined; : undefined;
const yulResult = makeYul
? await this.processYulOutput(outputFilename, asmResult, backendOptions.produceYul)
: undefined;
asmResult.dirPath = dirPath; asmResult.dirPath = dirPath;
if (!asmResult.compilationOptions) asmResult.compilationOptions = options; if (!asmResult.compilationOptions) asmResult.compilationOptions = options;
asmResult.downloads = downloads; asmResult.downloads = downloads;
@@ -2565,6 +2601,8 @@ export class BaseCompiler {
asmResult.clojureMacroExpOutput = clojureMacroExpResult; asmResult.clojureMacroExpOutput = clojureMacroExpResult;
asmResult.yulOutput = yulResult;
if (asmResult.code !== 0) { if (asmResult.code !== 0) {
return [{...asmResult, asm: '<Compilation failed>'}, [], []]; return [{...asmResult, asm: '<Compilation failed>'}, [], []];
} }

View File

@@ -85,6 +85,7 @@ export class ResolcCompiler extends BaseCompiler {
this.compiler.irArg = []; this.compiler.irArg = [];
this.compiler.supportsIrView = true; this.compiler.supportsIrView = true;
this.compiler.supportsIrViewOptToggleOption = true; this.compiler.supportsIrViewOptToggleOption = true;
this.compiler.supportsYulView = this.inputIs(InputKind.Solidity);
} }
override getSharedLibraryPathsAsArguments(): string[] { override getSharedLibraryPathsAsArguments(): string[] {

View File

@@ -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 CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME = 'clojuremacroexp' as const;
export const DEVICE_VIEW_COMPONENT_NAME = 'device' as const; export const DEVICE_VIEW_COMPONENT_NAME = 'device' as const;
export const EXPLAIN_VIEW_COMPONENT_NAME = 'explain' 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}; export type StateWithLanguage = {lang: string};
// TODO(#7808): Normalize state types to reduce duplication (see #4490) // TODO(#7808): Normalize state types to reduce duplication (see #4490)
@@ -356,6 +357,15 @@ export type PopulatedExplainViewState = StateWithId & {
treeid: number; 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 * 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. * 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; [CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME]: EmptyClojureMacroExpViewState | PopulatedClojureMacroExpViewState;
[DEVICE_VIEW_COMPONENT_NAME]: EmptyDeviceViewState | PopulatedDeviceViewState; [DEVICE_VIEW_COMPONENT_NAME]: EmptyDeviceViewState | PopulatedDeviceViewState;
[EXPLAIN_VIEW_COMPONENT_NAME]: EmptyExplainViewState | PopulatedExplainViewState; [EXPLAIN_VIEW_COMPONENT_NAME]: EmptyExplainViewState | PopulatedExplainViewState;
[YUL_VIEW_COMPONENT_NAME]: EmptyYulViewState | PopulatedYulViewState;
} }
/** /**

View File

@@ -66,6 +66,7 @@ import {
TOOL_COMPONENT_NAME, TOOL_COMPONENT_NAME,
TOOL_INPUT_VIEW_COMPONENT_NAME, TOOL_INPUT_VIEW_COMPONENT_NAME,
TREE_COMPONENT_NAME, TREE_COMPONENT_NAME,
YUL_VIEW_COMPONENT_NAME,
} from './components.interfaces.js'; } from './components.interfaces.js';
import {GccDumpViewState} from './panes/gccdump-view.interfaces.js'; import {GccDumpViewState} from './panes/gccdump-view.interfaces.js';
import {SentryCapture} from './sentry.js'; import {SentryCapture} from './sentry.js';
@@ -782,6 +783,38 @@ export function getHaskellCmmViewWith(
}; };
} }
/** Get an empty Yul view component. */
export function getYulView(): ComponentConfig<typeof YUL_VIEW_COMPONENT_NAME> {
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<typeof YUL_VIEW_COMPONENT_NAME> {
return {
type: 'component',
componentName: YUL_VIEW_COMPONENT_NAME,
componentState: {
id,
source,
yulOutput,
compilerName,
editorid,
treeid,
},
};
}
/** Get an empty gnat debug tree view component. */ /** Get an empty gnat debug tree view component. */
export function getGnatDebugTreeView(): ComponentConfig<typeof GNAT_DEBUG_TREE_VIEW_COMPONENT_NAME> { export function getGnatDebugTreeView(): ComponentConfig<typeof GNAT_DEBUG_TREE_VIEW_COMPONENT_NAME> {
return { return {
@@ -1233,6 +1266,7 @@ function validateComponentState(componentName: string, state: any): boolean {
case RUST_HIR_VIEW_COMPONENT_NAME: case RUST_HIR_VIEW_COMPONENT_NAME:
case CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME: case CLOJURE_MACRO_EXP_VIEW_COMPONENT_NAME:
case DEVICE_VIEW_COMPONENT_NAME: case DEVICE_VIEW_COMPONENT_NAME:
case YUL_VIEW_COMPONENT_NAME:
return true; return true;
default: default:

View File

@@ -26,6 +26,7 @@ import {ClangirBackendOptions} from '../types/compilation/clangir.interfaces.js'
import {CompilationResult} from '../types/compilation/compilation.interfaces.js'; import {CompilationResult} from '../types/compilation/compilation.interfaces.js';
import {LLVMIrBackendOptions} from '../types/compilation/ir.interfaces.js'; import {LLVMIrBackendOptions} from '../types/compilation/ir.interfaces.js';
import {OptPipelineBackendOptions} from '../types/compilation/opt-pipeline-output.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 {CompilerInfo} from '../types/compiler.interfaces.js';
import {Language, LanguageKey} from '../types/languages.interfaces.js'; import {Language, LanguageKey} from '../types/languages.interfaces.js';
import {MessageWithLocation} from '../types/resultline/resultline.interfaces.js'; import {MessageWithLocation} from '../types/resultline/resultline.interfaces.js';
@@ -160,6 +161,9 @@ export type EventMap = {
rustMirViewOpened: (compilerId: number) => void; rustMirViewOpened: (compilerId: number) => void;
clojureMacroExpViewClosed: (compilerId: number) => void; clojureMacroExpViewClosed: (compilerId: number) => void;
clojureMacroExpViewOpened: (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 // TODO: There are no emitters for this event
selectLine: (editorId: number, lineNumber: number) => void; selectLine: (editorId: number, lineNumber: number) => void;
settingsChange: (newSettings: SiteSettings) => void; settingsChange: (newSettings: SiteSettings) => void;

View File

@@ -59,6 +59,7 @@ import {
TOOL_COMPONENT_NAME, TOOL_COMPONENT_NAME,
TOOL_INPUT_VIEW_COMPONENT_NAME, TOOL_INPUT_VIEW_COMPONENT_NAME,
TREE_COMPONENT_NAME, TREE_COMPONENT_NAME,
YUL_VIEW_COMPONENT_NAME,
} from './components.interfaces.js'; } from './components.interfaces.js';
import {EventHub} from './event-hub.js'; import {EventHub} from './event-hub.js';
import {EventMap} from './event-map.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 {Tool} from './panes/tool.js';
import {ToolInputView} from './panes/tool-input-view.js'; import {ToolInputView} from './panes/tool-input-view.js';
import {Tree, TreeState} from './panes/tree.js'; import {Tree, TreeState} from './panes/tree.js';
import {Yul as YulView} from './panes/yul-view.js';
type GLC = GoldenLayout.Container; type GLC = GoldenLayout.Container;
@@ -173,6 +175,7 @@ export class Hub {
this.conformanceViewFactory(c, s), this.conformanceViewFactory(c, s),
); );
layout.registerComponent(EXPLAIN_VIEW_COMPONENT_NAME, (c: GLC, s: any) => this.explainViewFactory(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( layout.eventHub.on(
'editorOpen', 'editorOpen',
@@ -604,4 +607,8 @@ export class Hub {
public explainViewFactory(container: GoldenLayout.Container, state: any): ExplainView { public explainViewFactory(container: GoldenLayout.Container, state: any): ExplainView {
return new ExplainView(this, container, state); return new ExplainView(this, container, state);
} }
public yulViewFactory(container: GoldenLayout.Container, state: InferComponentState<YulView>): YulView {
return new YulView(this, container, state);
}
} }

View File

@@ -75,6 +75,7 @@ import fileSaver from 'file-saver';
import {escapeHTML, splitArguments} from '../../shared/common-utils.js'; import {escapeHTML, splitArguments} from '../../shared/common-utils.js';
import {ClangirBackendOptions} from '../../types/compilation/clangir.interfaces.js'; import {ClangirBackendOptions} from '../../types/compilation/clangir.interfaces.js';
import {LLVMIrBackendOptions} from '../../types/compilation/ir.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 {CompilerOutputOptions} from '../../types/features/filters.interfaces.js';
import {InstructionSet} from '../../types/instructionsets.js'; import {InstructionSet} from '../../types/instructionsets.js';
import {LanguageKey} from '../../types/languages.interfaces.js'; import {LanguageKey} from '../../types/languages.interfaces.js';
@@ -194,6 +195,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
private haskellStgButton: JQuery<HTMLButtonElement>; private haskellStgButton: JQuery<HTMLButtonElement>;
private haskellCmmButton: JQuery<HTMLButtonElement>; private haskellCmmButton: JQuery<HTMLButtonElement>;
private clojureMacroExpButton: JQuery<HTMLButtonElement>; private clojureMacroExpButton: JQuery<HTMLButtonElement>;
private yulButton: JQuery<HTMLButtonElement>;
private gccDumpButton: JQuery<HTMLButtonElement>; private gccDumpButton: JQuery<HTMLButtonElement>;
private cfgButton: JQuery<HTMLButtonElement>; private cfgButton: JQuery<HTMLButtonElement>;
private explainButton: JQuery<HTMLButtonElement>; private explainButton: JQuery<HTMLButtonElement>;
@@ -271,9 +273,11 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
private haskellStgViewOpen: boolean; private haskellStgViewOpen: boolean;
private haskellCmmViewOpen: boolean; private haskellCmmViewOpen: boolean;
private clojureMacroExpViewOpen: boolean; private clojureMacroExpViewOpen: boolean;
private yulViewOpen: boolean;
private ppOptions: PPOptions; private ppOptions: PPOptions;
private llvmIrOptions: LLVMIrBackendOptions; private llvmIrOptions: LLVMIrBackendOptions;
private clangirOptions: ClangirBackendOptions; private clangirOptions: ClangirBackendOptions;
private yulOptions: YulBackendOptions;
private optPipelineOptions: OptPipelineBackendOptions; private optPipelineOptions: OptPipelineBackendOptions;
private isOutputOpened: boolean; private isOutputOpened: boolean;
private mouseMoveThrottledFunction?: ((e: monaco.editor.IEditorMouseEvent) => void) & _.Cancelable; private mouseMoveThrottledFunction?: ((e: monaco.editor.IEditorMouseEvent) => void) & _.Cancelable;
@@ -636,6 +640,17 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
); );
}; };
const createYulView = () => {
return Components.getYulViewWith(
this.id,
this.source,
this.lastResult?.yulOutput,
this.getCompilerName(),
this.sourceEditorId ?? 0,
this.sourceTreeId ?? 0,
);
};
const createGccDumpView = () => { const createGccDumpView = () => {
return Components.getGccDumpViewWith( return Components.getGccDumpViewWith(
this.id, this.id,
@@ -920,6 +935,18 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
insertPoint.addChild(createClojureMacroExpView()); insertPoint.addChild(createClojureMacroExpView());
}); });
createDragSource(this.container.layoutManager, this.yulButton, () => 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( createDragSource(this.container.layoutManager, this.gccDumpButton, () => createGccDumpView()).on(
'dragStart', 'dragStart',
hidePaneAdder, hidePaneAdder,
@@ -1316,6 +1343,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
produceHaskellStg: this.haskellStgViewOpen, produceHaskellStg: this.haskellStgViewOpen,
produceHaskellCmm: this.haskellCmmViewOpen, produceHaskellCmm: this.haskellCmmViewOpen,
produceClojureMacroExp: this.clojureMacroExpViewOpen, produceClojureMacroExp: this.clojureMacroExpViewOpen,
produceYul: this.yulViewOpen ? this.yulOptions : null,
overrides: this.getCurrentState().overrides, overrides: this.getCurrentState().overrides,
}, },
filters: this.getEffectiveFilters(), filters: this.getEffectiveFilters(),
@@ -2226,6 +2254,30 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
} }
} }
onYulViewOpened(id: number): void {
if (this.id === id) {
this.yulButton.prop('disabled', true);
this.yulViewOpen = true;
this.compile();
}
}
onYulViewClosed(id: number): void {
if (this.id === id) {
this.yulButton.prop('disabled', false);
this.yulViewOpen = false;
}
}
onYulViewOptionsUpdated(id: number, options: YulBackendOptions, recompile: boolean): void {
if (this.id === id) {
this.yulOptions = options;
if (recompile) {
this.compile();
}
}
}
onGccDumpUIInit(id: number): void { onGccDumpUIInit(id: number): void {
if (this.id === id) { if (this.id === id) {
this.compile(); this.compile();
@@ -2415,6 +2467,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
this.haskellStgButton = this.domRoot.find('.btn.view-haskellStg'); this.haskellStgButton = this.domRoot.find('.btn.view-haskellStg');
this.haskellCmmButton = this.domRoot.find('.btn.view-haskellCmm'); this.haskellCmmButton = this.domRoot.find('.btn.view-haskellCmm');
this.clojureMacroExpButton = this.domRoot.find('.btn.view-clojuremacroexp'); this.clojureMacroExpButton = this.domRoot.find('.btn.view-clojuremacroexp');
this.yulButton = this.domRoot.find('.btn.view-yul');
this.gccDumpButton = this.domRoot.find('.btn.view-gccdump'); this.gccDumpButton = this.domRoot.find('.btn.view-gccdump');
this.cfgButton = this.domRoot.find('.btn.view-cfg'); this.cfgButton = this.domRoot.find('.btn.view-cfg');
this.explainButton = this.domRoot.find('.btn.view-explain'); this.explainButton = this.domRoot.find('.btn.view-explain');
@@ -2699,6 +2752,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
this.rustMacroExpButton.prop('disabled', this.rustMacroExpViewOpen); this.rustMacroExpButton.prop('disabled', this.rustMacroExpViewOpen);
this.rustHirButton.prop('disabled', this.rustHirViewOpen); this.rustHirButton.prop('disabled', this.rustHirViewOpen);
this.clojureMacroExpButton.prop('disabled', this.clojureMacroExpViewOpen); this.clojureMacroExpButton.prop('disabled', this.clojureMacroExpViewOpen);
this.yulButton.prop('disabled', this.yulViewOpen);
this.gccDumpButton.prop('disabled', this.gccDumpViewOpen); this.gccDumpButton.prop('disabled', this.gccDumpViewOpen);
this.gnatDebugTreeButton.prop('disabled', this.gnatDebugTreeViewOpen); this.gnatDebugTreeButton.prop('disabled', this.gnatDebugTreeViewOpen);
this.gnatDebugButton.prop('disabled', this.gnatDebugViewOpen); this.gnatDebugButton.prop('disabled', this.gnatDebugViewOpen);
@@ -2720,6 +2774,7 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
this.haskellStgButton.toggle(!!this.compiler.supportsHaskellStgView); this.haskellStgButton.toggle(!!this.compiler.supportsHaskellStgView);
this.haskellCmmButton.toggle(!!this.compiler.supportsHaskellCmmView); this.haskellCmmButton.toggle(!!this.compiler.supportsHaskellCmmView);
this.clojureMacroExpButton.toggle(!!this.compiler.supportsClojureMacroExpView); this.clojureMacroExpButton.toggle(!!this.compiler.supportsClojureMacroExpView);
this.yulButton.toggle(!!this.compiler.supportsYulView);
// TODO(jeremy-rifkin): Disable cfg button when binary mode is set? // TODO(jeremy-rifkin): Disable cfg button when binary mode is set?
this.cfgButton.toggle(!!this.compiler.supportsCfg); this.cfgButton.toggle(!!this.compiler.supportsCfg);
this.gccDumpButton.toggle(!!this.compiler.supportsGccDump); this.gccDumpButton.toggle(!!this.compiler.supportsGccDump);
@@ -2898,6 +2953,9 @@ export class Compiler extends MonacoPane<monaco.editor.IStandaloneCodeEditor, Co
this.eventHub.on('haskellCmmViewClosed', this.onHaskellCmmViewClosed, this); this.eventHub.on('haskellCmmViewClosed', this.onHaskellCmmViewClosed, this);
this.eventHub.on('clojureMacroExpViewOpened', this.onClojureMacroExpViewOpened, this); this.eventHub.on('clojureMacroExpViewOpened', this.onClojureMacroExpViewOpened, this);
this.eventHub.on('clojureMacroExpViewClosed', this.onClojureMacroExpViewClosed, this); this.eventHub.on('clojureMacroExpViewClosed', this.onClojureMacroExpViewClosed, this);
this.eventHub.on('yulViewOpened', this.onYulViewOpened, this);
this.eventHub.on('yulViewClosed', this.onYulViewClosed, this);
this.eventHub.on('yulViewOptionsUpdated', this.onYulViewOptionsUpdated, this);
this.eventHub.on('outputOpened', this.onOutputOpened, this); this.eventHub.on('outputOpened', this.onOutputOpened, this);
this.eventHub.on('outputClosed', this.onOutputClosed, this); this.eventHub.on('outputClosed', this.onOutputClosed, this);

View File

@@ -38,6 +38,7 @@ export enum DiffType {
RustMacroExpOutput = 11, RustMacroExpOutput = 11,
RustHirOutput = 12, RustHirOutput = 12,
ClojureMacroExpOutput = 13, ClojureMacroExpOutput = 13,
YulOutput = 14,
} }
export type DiffState = { export type DiffState = {

View File

@@ -175,6 +175,11 @@ class DiffStateObject {
{text: "<select 'Add new...' → 'Clojure Macro Expansion' in this compiler's pane>"}, {text: "<select 'Add new...' → 'Clojure Macro Expansion' in this compiler's pane>"},
]; ];
break; break;
case DiffType.YulOutput:
output = this.result.yulOutput || [
{text: "<select 'Add new...' → 'Yul (Solidity IR)' in this compiler's pane>"},
];
break;
} }
} }
this.model.setValue(output.map(x => x.text).join('\n')); this.model.setValue(output.map(x => x.text).join('\n'));
@@ -481,6 +486,9 @@ export class Diff extends MonacoPane<monaco.editor.IStandaloneDiffEditor, DiffSt
if (compiler.supportsClojureMacroExpView) { if (compiler.supportsClojureMacroExpView) {
options.push({id: DiffType.ClojureMacroExpOutput.toString(), name: 'Clojure Macro Expansion'}); options.push({id: DiffType.ClojureMacroExpOutput.toString(), name: 'Clojure Macro Expansion'});
} }
if (compiler.supportsYulView) {
options.push({id: DiffType.YulOutput.toString(), name: 'Yul (Solidity IR)'});
}
} }
const lhsoptions = this.getDiffableOptions(this.selectize.lhs, lhsextraoptions); const lhsoptions = this.getDiffableOptions(this.selectize.lhs, lhsextraoptions);

View File

@@ -0,0 +1,30 @@
// 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.
export interface YulState {
yulOutput: any;
// Options and filters.
'filter-debug-info'?: boolean;
}

175
static/panes/yul-view.ts Normal file
View File

@@ -0,0 +1,175 @@
// 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 {Container} from 'golden-layout';
import $ from 'jquery';
import * as monaco from 'monaco-editor';
import _ from 'underscore';
import {CompilationResult} from '../../types/compilation/compilation.interfaces.js';
import {YulBackendOptions} from '../../types/compilation/yul.interfaces.js';
import {CompilerInfo} from '../../types/compiler.interfaces.js';
import {Hub} from '../hub.js';
import {extendConfig} from '../monaco-config.js';
import {Toggles} from '../widgets/toggles.js';
import {MonacoPaneState} from './pane.interfaces.js';
import {MonacoPane} from './pane.js';
import {YulState} from './yul-view.interfaces.js';
export class Yul extends MonacoPane<monaco.editor.IStandaloneCodeEditor, YulState> {
private filters: Toggles;
private lastOptions: YulBackendOptions = {
filterDebugInfo: true,
};
constructor(hub: Hub, container: Container, state: YulState & MonacoPaneState) {
super(hub, container, state);
if (state.yulOutput) {
this.showYulResults(state.yulOutput);
}
this.onOptionsChange(true);
}
override getInitialHTML(): string {
return $('#yul').html();
}
override createEditor(editorRoot: HTMLElement): void {
this.editor = monaco.editor.create(
editorRoot,
extendConfig({
language: 'yul',
readOnly: true,
glyphMargin: true,
lineNumbersMinChars: 3,
}),
);
}
override getPrintName() {
return 'Yul Output';
}
override getDefaultPaneName(): string {
return 'Yul (Solidity IR) Viewer';
}
override registerButtons(state: YulState): void {
super.registerButtons(state);
this.filters = new Toggles(this.domRoot.find('.filters'), state as unknown as Record<string, boolean>);
this.filters.on('change', () => this.onOptionsChange());
}
updateButtons(compiler: CompilerInfo | null): void {
this.filters.enableToggle('filter-debug-info', !!compiler?.supportsYulView);
}
override registerCallbacks(): void {
const throttleFunction = _.throttle(
(event: monaco.editor.ICursorSelectionChangedEvent) => this.onDidChangeCursorSelection(event),
500,
);
this.editor.onDidChangeCursorSelection(event => throttleFunction(event));
this.eventHub.emit('yulViewOpened', this.compilerInfo.compilerId);
this.eventHub.emit('requestSettings');
}
override getCurrentState(): MonacoPaneState {
return {
...super.getCurrentState(),
...this.filters.get(),
};
}
onOptionsChange(force = false): void {
const filters = this.filters.get();
const newOptions: YulBackendOptions = {
filterDebugInfo: filters['filter-debug-info'],
};
let changed = false;
for (const key in newOptions) {
if (newOptions[key as keyof YulBackendOptions] !== this.lastOptions[key as keyof Yul]) {
changed = true;
break;
}
}
this.lastOptions = newOptions;
if (changed || force) {
this.eventHub.emit('yulViewOptionsUpdated', this.compilerInfo.compilerId, newOptions, true);
}
}
override onCompileResult(compilerId: number, compiler: CompilerInfo, result: CompilationResult): void {
if (this.compilerInfo.compilerId !== compilerId) return;
if (result.yulOutput) {
this.showYulResults(result.yulOutput);
} else if (compiler.supportsYulView) {
this.showYulResults([{text: '<No output>'}]);
}
}
override onCompiler(
compilerId: number,
compiler: CompilerInfo | null,
options: string,
editorId?: number,
treeId?: number,
): void {
if (this.compilerInfo.compilerId === compilerId) {
this.compilerInfo.compilerName = compiler?.name || '';
this.compilerInfo.editorId = editorId;
this.compilerInfo.treeId = treeId;
this.updateTitle();
this.updateButtons(compiler);
if (!compiler?.supportsYulView) {
const text = compiler?.name.toLowerCase().includes('resolc')
? '<Yul output is only supported for this compiler when the input language is Solidity>'
: '<Yul output is not supported for this compiler>';
this.showYulResults([{text}]);
}
}
}
showYulResults(result: any[]): void {
const newValue = result.length ? _.pluck(result, 'text').join('\n') : '<No Yul generated>';
this.editor.getModel()?.setValue(newValue);
if (!this.isAwaitingInitialResults) {
if (this.selection) {
this.editor.setSelection(this.selection);
this.editor.revealLinesInCenter(this.selection.selectionStartLineNumber, this.selection.endLineNumber);
}
this.isAwaitingInitialResults = true;
}
}
override close(): void {
this.eventHub.unsubscribe();
this.eventHub.emit('yulViewClosed', this.compilerInfo.compilerId);
this.editor.dispose();
}
}

View File

@@ -58,7 +58,7 @@ describe('Resolc', () => {
expectedFilenameWithoutExtension: string, expectedFilenameWithoutExtension: string,
): void { ): void {
const defaultOutputFilename = `${expectedFilenameWithoutExtension}.pvmasm`; const defaultOutputFilename = `${expectedFilenameWithoutExtension}.pvmasm`;
expect(compiler.getOutputFilename(path.normalize('test/resolc'))).toEqual(defaultOutputFilename); expect(compiler.getOutputFilename(path.normalize(path.dirname(inputFilename)))).toEqual(defaultOutputFilename);
let llvmIrBackendOptions = makeFakeLlvmIrBackendOptions({showOptimized: true}); let llvmIrBackendOptions = makeFakeLlvmIrBackendOptions({showOptimized: true});
expect(compiler.getIrOutputFilename(inputFilename, undefined, llvmIrBackendOptions)).toEqual( expect(compiler.getIrOutputFilename(inputFilename, undefined, llvmIrBackendOptions)).toEqual(

View File

@@ -42,6 +42,7 @@ import {ClangirBackendOptions} from './clangir.interfaces.js';
import {ConfiguredOverrides} from './compiler-overrides.interfaces.js'; import {ConfiguredOverrides} from './compiler-overrides.interfaces.js';
import {LLVMIrBackendOptions} from './ir.interfaces.js'; import {LLVMIrBackendOptions} from './ir.interfaces.js';
import {OptPipelineBackendOptions, OptPipelineOutput} from './opt-pipeline-output.interfaces.js'; import {OptPipelineBackendOptions, OptPipelineOutput} from './opt-pipeline-output.interfaces.js';
import {YulBackendOptions} from './yul.interfaces.js';
export type ActiveTool = { export type ActiveTool = {
id: string; id: string;
@@ -117,6 +118,7 @@ export type CompilationRequestOptions = {
produceHaskellStg?: boolean; produceHaskellStg?: boolean;
produceHaskellCmm?: boolean; produceHaskellCmm?: boolean;
produceClojureMacroExp?: boolean; produceClojureMacroExp?: boolean;
produceYul?: YulBackendOptions | null;
cmakeArgs?: string; cmakeArgs?: string;
customOutputFilename?: string; customOutputFilename?: string;
overrides?: ConfiguredOverrides; overrides?: ConfiguredOverrides;
@@ -216,6 +218,8 @@ export type CompilationResult = {
clojureMacroExpOutput?: ResultLine[]; clojureMacroExpOutput?: ResultLine[];
yulOutput?: ResultLine[];
forceBinaryView?: boolean; forceBinaryView?: boolean;
artifacts?: Artifact[]; artifacts?: Artifact[];

View File

@@ -0,0 +1,27 @@
// 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.
export type YulBackendOptions = {
filterDebugInfo: boolean;
};

View File

@@ -91,6 +91,7 @@ export type CompilerInfo = {
supportsHaskellStgView?: boolean; supportsHaskellStgView?: boolean;
supportsHaskellCmmView?: boolean; supportsHaskellCmmView?: boolean;
supportsClojureMacroExpView?: boolean; supportsClojureMacroExpView?: boolean;
supportsYulView?: boolean;
supportsCfg?: boolean; supportsCfg?: boolean;
supportsGnatDebugViews?: boolean; supportsGnatDebugViews?: boolean;
supportsLibraryCodeFilter?: boolean; supportsLibraryCodeFilter?: boolean;

View File

@@ -65,6 +65,7 @@ mixin newPaneButton(classId, text, title, icon)
+newPaneButton("view-haskellStg", "GHC STG", "Show GHC STG Intermediate Representation", "fas fa-water") +newPaneButton("view-haskellStg", "GHC STG", "Show GHC STG Intermediate Representation", "fas fa-water")
+newPaneButton("view-haskellCmm", "GHC Cmm", "Show GHC Cmm Intermediate Representation", "fas fa-water") +newPaneButton("view-haskellCmm", "GHC Cmm", "Show GHC Cmm Intermediate Representation", "fas fa-water")
+newPaneButton("view-clojuremacroexp", "Clojure Macro Expansion", "Show Clojure macro expansion", "fas fa-arrows-alt") +newPaneButton("view-clojuremacroexp", "Clojure Macro Expansion", "Show Clojure macro expansion", "fas fa-arrows-alt")
+newPaneButton("view-yul", "Yul (Solidity IR)", "Show Solidity Intermediate Representation", "fas fa-align-center")
+newPaneButton("view-gccdump", "GCC Tree/RTL", "Show GCC Tree/RTL dump", "fas fa-tree") +newPaneButton("view-gccdump", "GCC Tree/RTL", "Show GCC Tree/RTL dump", "fas fa-tree")
+newPaneButton("view-gnatdebugtree", "GNAT Debug Tree", "Show GNAT debug tree", "fas fa-tree") +newPaneButton("view-gnatdebugtree", "GNAT Debug Tree", "Show GNAT debug tree", "fas fa-tree")
+newPaneButton("view-gnatdebug", "GNAT Debug Expanded Code", "Show GNAT debug expanded code", "fas fa-tree") +newPaneButton("view-gnatdebug", "GNAT Debug Expanded Code", "Show GNAT debug expanded code", "fas fa-tree")

View File

@@ -0,0 +1,17 @@
mixin optionButton(bind, isActive, text, title)
.button-checkbox
button(type="button" class="dropdown-item btn btn-sm btn-light" + (isActive ? " active" : "") title=title data-bind=bind aria-pressed=isActive ? "true" : "false")
span #{text}
input.d-none(type="checkbox" checked=isActive)
#yul
.top-bar.btn-toolbar.bg-light(role="toolbar")
include ../../font-size
.btn-group.btn-group-sm.filters(role="group")
button.btn.btn-sm.btn-light.dropdown-toggle(type="button" title="Yul Output Filters" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Set output filters")
span.fas.fa-filter
span.hideable Filters
.dropdown-menu
+optionButton("filter-debug-info", true, "Hide Debug Info", "Filter debug info intrinsics")
div.yul-body
.monaco-placeholder

View File

@@ -40,6 +40,8 @@ mixin monacopane(id)
include panes/explain include panes/explain
include panes/yul
include widgets/compiler-selector include widgets/compiler-selector
include widgets/libs-dropdown include widgets/libs-dropdown
@@ -87,3 +89,5 @@ mixin monacopane(id)
+monacopane("rusthir") +monacopane("rusthir")
+monacopane("clojuremacroexp") +monacopane("clojuremacroexp")
+monacopane("yul")