diff --git a/ui/frontend/.prettierignore b/ui/frontend/.prettierignore index 3be14a17..63903541 100644 --- a/ui/frontend/.prettierignore +++ b/ui/frontend/.prettierignore @@ -19,9 +19,13 @@ node_modules !Header.tsx !HelpExample.tsx !Notifications.tsx +!Output/Assembly.tsx +!Output/LlvmIr.tsx !Output/OutputPrism.tsx +!Output/PaneWithCode.tsx !Output/PaneWithMir.tsx !Output/SimplePane.tsx +!Output/WarnAboutNoSymbols.tsx !PopButton.tsx !Prism.tsx !Stdin.tsx diff --git a/ui/frontend/Output.tsx b/ui/frontend/Output.tsx index 5207a2cd..c5afdd1e 100644 --- a/ui/frontend/Output.tsx +++ b/ui/frontend/Output.tsx @@ -4,15 +4,17 @@ import { changeFocus } from './reducers/output/meta'; import { Focus } from './types'; import Execute from './Output/Execute'; +import Assembly from './Output/Assembly'; import Gist from './Output/Gist'; -import Section from './Output/Section'; -import SimplePane, { SimplePaneProps } from './Output/SimplePane'; +import SimplePane from './Output/SimplePane'; import PaneWithMir from './Output/PaneWithMir'; +import PaneWithCode from './Output/PaneWithCode'; import * as selectors from './selectors'; import { useAppDispatch, useAppSelector } from './hooks'; import * as styles from './Output.module.css'; import Stdin from './Stdin'; +import LlvmIr from './Output/LlvmIr'; const Tab: React.FC = ({ kind, focus, label, onClick, tabProps }) => { if (selectors.hasProperties(tabProps)) { @@ -35,16 +37,6 @@ interface TabProps { tabProps: object; } -const PaneWithCode: React.FC = ({ code, ...rest }) => ( - -
{code}
-
-); - -interface PaneWithCodeProps extends SimplePaneProps { - code?: string; -} - const Output: React.FC = () => { const somethingToShow = useAppSelector(selectors.getSomethingToShow); const { meta: { focus }, execute, format, clippy, miri, macroExpansion, assembly, llvmIr, mir, hir, wasm, gist } = @@ -83,8 +75,8 @@ const Output: React.FC = () => { {focus === Focus.Clippy && } {focus === Focus.Miri && } {focus === Focus.MacroExpansion && } - {focus === Focus.Asm && } - {focus === Focus.LlvmIr && } + {focus === Focus.Asm && } + {focus === Focus.LlvmIr && } {focus === Focus.Mir && } {focus === Focus.Hir && } {focus === Focus.Wasm && } diff --git a/ui/frontend/Output/Assembly.tsx b/ui/frontend/Output/Assembly.tsx new file mode 100644 index 00000000..d16ff3f2 --- /dev/null +++ b/ui/frontend/Output/Assembly.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { useAppSelector } from '../hooks'; +import * as selectors from '../selectors'; +import PaneWithCode from './PaneWithCode'; +import WarnAboutNoSymbols from './WarnAboutNoSymbols'; + +const Assembly: React.FC = () => { + const assembly = useAppSelector((state) => state.output.assembly); + const isAssemblyInProgress = useAppSelector(selectors.isAssemblyInProgressSelector); + const hasAssemblySymbols = useAppSelector(selectors.hasAssemblySymbolsSelector); + + return ( + + + + ); +}; + +export default Assembly; diff --git a/ui/frontend/Output/LlvmIr.tsx b/ui/frontend/Output/LlvmIr.tsx new file mode 100644 index 00000000..96cb2b2d --- /dev/null +++ b/ui/frontend/Output/LlvmIr.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { useAppSelector } from '../hooks'; +import * as selectors from '../selectors'; +import PaneWithCode from './PaneWithCode'; +import WarnAboutNoSymbols from './WarnAboutNoSymbols'; + +const LlvmIr: React.FC = () => { + const llvmIr = useAppSelector((state) => state.output.llvmIr); + const isLlvmIrInProgress = useAppSelector(selectors.isLlvmIrInProgressSelector); + const hasLlvmIrSymbols = useAppSelector(selectors.hasLlvmIrSymbolsSelector); + + return ( + + + + ); +}; + +export default LlvmIr; diff --git a/ui/frontend/Output/PaneWithCode.tsx b/ui/frontend/Output/PaneWithCode.tsx new file mode 100644 index 00000000..a1cd3a84 --- /dev/null +++ b/ui/frontend/Output/PaneWithCode.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import Section from './Section'; +import SimplePane, { SimplePaneProps } from './SimplePane'; + +export interface PaneWithCodeProps extends SimplePaneProps { + code?: string; +} + +const PaneWithCode: React.FC = ({ children, code, ...rest }) => ( + +
+ {code} +
+ {children} +
+); + +export default PaneWithCode; diff --git a/ui/frontend/Output/WarnAboutNoSymbols.tsx b/ui/frontend/Output/WarnAboutNoSymbols.tsx new file mode 100644 index 00000000..5f77c84e --- /dev/null +++ b/ui/frontend/Output/WarnAboutNoSymbols.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import Section from './Section'; + +export interface WarnAboutNoSymbolsProps { + isInProgress: boolean; + hasSymbols: boolean; + name: string; +} + +const WarnAboutNoSymbols: React.FC = ({ + isInProgress, + hasSymbols, + name, +}) => { + const warnAboutNoSymbols = !isInProgress && !hasSymbols; + + if (!warnAboutNoSymbols) { + return null; + } + + return ( +
+ No symbols detected — they may have been optimized away. + {'\n'} + Add the #[unsafe(no_mangle)] attribute to + {'\n'} + functions you want to see {name} for. Generic functions + {'\n'} + only generate {name} when concrete types are provided. +
+ ); +}; + +export default WarnAboutNoSymbols; diff --git a/ui/frontend/selectors/index.spec.ts b/ui/frontend/selectors/index.spec.ts index 98d1f51b..89918768 100644 --- a/ui/frontend/selectors/index.spec.ts +++ b/ui/frontend/selectors/index.spec.ts @@ -1,6 +1,10 @@ import reducer from '../reducers'; import { editCode } from '../reducers/code'; -import { hasMainFunctionSelector } from './index'; +import { + hasAssemblySymbolsSelector, + hasLlvmIrSymbolsSelector, + hasMainFunctionSelector, +} from './index'; const buildState = (code: string) => reducer(undefined, editCode(code)); @@ -64,3 +68,78 @@ describe('checking for a main function', () => { expect(doMainFunctionSelector('fn main(/* comment */) {')).toBe(true); }); }); + +const doHasAssemblySymbolSelector = (code: string) => { + const state = reducer( + { output: { assembly: { code, requestsInProgress: 0 } } }, + { type: 'test' }, + ); + return hasAssemblySymbolsSelector(state); +}; + +describe('checking for symbols in assembly output', () => { + test('empty code has no symbols', () => { + expect(doHasAssemblySymbolSelector('')).toBe(false); + }); + + test('instructions are not symbols', () => { + // x86_64 + expect(doHasAssemblySymbolSelector(' movl %edi, 4(%rsp)')).toBe(false); + // arm + expect(doHasAssemblySymbolSelector(' sub sp, sp, #32')).toBe(false); + }); + + test('mangled symbols are symbols', () => { + expect(doHasAssemblySymbolSelector('_ZN10playground3add17h903bea7e047dfb9fE:')).toBe(true); + }); + + test('unmangled symbols are symbols', () => { + expect(doHasAssemblySymbolSelector('playground::add:')).toBe(true); + }); + + test('unmangled symbols from traits are symbols', () => { + expect( + doHasAssemblySymbolSelector( + ' as rand_core::block::BlockRngCore>::generate:', + ), + ).toBe(true); + }); + + test('symbols with comments are symbols', () => { + // x86_64 + expect(doHasAssemblySymbolSelector('add: # @add')).toBe( + true, + ); + // arm + expect(doHasAssemblySymbolSelector('add: // @add')).toBe( + true, + ); + }); +}); + +const doHasLlvmIrSymbolsSelector = (code: string) => { + const state = reducer({ output: { llvmIr: { code, requestsInProgress: 0 } } }, { type: 'test' }); + return hasLlvmIrSymbolsSelector(state); +}; + +describe('checking for symbols in LLVM IR output', () => { + test('empty code has no symbols', () => { + expect(doHasLlvmIrSymbolsSelector('')).toBe(false); + }); + + test('metadata is not a symbol', () => { + expect( + doHasLlvmIrSymbolsSelector('source_filename = "playground.d1ee58e2761c15fe-cgu.0"'), + ).toBe(false); + expect(doHasLlvmIrSymbolsSelector('!llvm.ident = !{!1}')).toBe(false); + expect( + doHasLlvmIrSymbolsSelector('!1 = !{!"rustc version 1.90.0-nightly (3048886e5 2025-07-30)"}'), + ).toBe(false); + }); + + test('a symbol is a symbol', () => { + expect( + doHasLlvmIrSymbolsSelector('define noundef i32 @add(i32 noundef %v) unnamed_addr #0 {'), + ).toBe(true); + }); +}); diff --git a/ui/frontend/selectors/index.ts b/ui/frontend/selectors/index.ts index 1357494d..8d27ebf1 100644 --- a/ui/frontend/selectors/index.ts +++ b/ui/frontend/selectors/index.ts @@ -518,6 +518,30 @@ export const compileRequestPayloadSelector = createSelector( }), ); +export const isAssemblyInProgressSelector = createSelector( + (state: State) => state.output.assembly, + asm => asm.requestsInProgress > 0, +); + +const ASSEMBLY_SYMBOLS_RE = /^[_a-zA-Z0-9<>, ]+:/m; + +export const hasAssemblySymbolsSelector = createSelector( + (state: State) => state.output.assembly, + asm => !!asm.code?.match(ASSEMBLY_SYMBOLS_RE), +); + +export const isLlvmIrInProgressSelector = createSelector( + (state: State) => state.output.llvmIr, + llvmIr => llvmIr.requestsInProgress > 0, +); + +const LLVMIR_SYMBOLS_RE = /^define.*@.*{/m; + +export const hasLlvmIrSymbolsSelector = createSelector( + (state: State) => state.output.llvmIr, + llvmIr => !!llvmIr.code?.match(LLVMIR_SYMBOLS_RE), +); + export const themeSelector = createSelector( (state: State) => state, (state) => state.configuration.theme,