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 ce-yul-view --- 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: "