Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
804 changes: 804 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
"devDependencies": {
"@eslint/js": "^9.34.0",
"@playwright/test": "^1.54.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@types/lodash": "^4.17.7",
"@types/node": "^24.3.0",
"@types/react": "^18.3.3",
Expand All @@ -76,6 +79,7 @@
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"husky": "^9.1.1",
"jsdom": "^27.0.0",
"lint-staged": "^16.1.6",
"postcss": "^8.4.39",
"prettier": "3.3.3",
Expand Down
98 changes: 88 additions & 10 deletions src/components/DebuggerControlls/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RefreshCcw, Play, StepForward, Pause } from "lucide-react";
import { RefreshCcw, Play, StepForward, SkipForward, Pause } from "lucide-react";
import { LoadingSpinner } from "../LoadingSpinner";
import { ProgramLoader } from "../ProgramLoader";
import { Button } from "../ui/button";
Expand All @@ -22,18 +22,51 @@ import { Separator } from "../ui/separator";
export const DebuggerControlls = () => {
const debuggerActions = useDebuggerActions();
const [error, setError] = useState<string>();
const { program, initialState, isProgramEditMode, isDebugFinished, pvmInitialized, isRunMode } = useAppSelector(
(state) => state.debugger,
);
const { program, initialState, isProgramEditMode, isDebugFinished, pvmInitialized, isRunMode, programPreviewResult } =
useAppSelector((state) => state.debugger);

const workers = useAppSelector((state) => state.workers);
const isLoading = useAppSelector(selectIsAnyWorkerLoading);
const dispatch = useAppDispatch();

const { currentInstruction } = workers[0] || {
currentInstruction: null,
currentState: initialState,
previousState: initialState,
const currentInstruction = programPreviewResult.find((x) => x.address === workers[0]?.currentPc);

const calculateStepsToExitBlock = (): number => {
if (!currentInstruction || !programPreviewResult) {
return 1;
}

// If we're already at the end of a block, step once
if (currentInstruction.block.isEnd) {
return 1;
}

// Find the current instruction in the program preview result
const currentIndex = programPreviewResult.findIndex((inst) => inst.address === currentInstruction.address);
if (currentIndex === -1) {
return 1;
}

// Count instructions remaining in the current block
let stepsInBlock = 1; // Count the step from current instruction

for (let i = currentIndex + 1; i < programPreviewResult.length; i++) {
const instruction = programPreviewResult[i];

// If we encounter a different block or the end of current block, stop counting
if (instruction.block.number !== currentInstruction.block.number) {
break;
}

stepsInBlock++;

// If this instruction is the end of the block, we're done
if (instruction.block.isEnd) {
break;
}
}

return stepsInBlock;
};

const onNext = async () => {
Expand All @@ -44,7 +77,7 @@ export const DebuggerControlls = () => {

try {
if (!currentInstruction) {
await dispatch(setAllWorkersCurrentState(initialState));
dispatch(setAllWorkersCurrentState(initialState));
}

// NOTE [ToDr] Despite settings "batched steps", when
Expand All @@ -60,6 +93,31 @@ export const DebuggerControlls = () => {
}
};

const onStepOverBlock = async () => {
if (!workers.length) {
setError("No workers initialized");
return;
}

try {
if (!currentInstruction) {
// perform the initial step
await onNext();
return;
}

const stepsToPerform = calculateStepsToExitBlock();
await dispatch(stepAllWorkers({ stepsToPerform })).unwrap();
dispatch(setIsProgramEditMode(false));
} catch (error) {
if (error instanceof Error || isSerializedError(error)) {
setError(error.message);
} else {
setError("Unknown error occured");
}
}
};

const handleRunProgram = async () => {
if (!workers.length) {
setError("No workers initialized");
Expand Down Expand Up @@ -149,8 +207,28 @@ export const DebuggerControlls = () => {
)}
<span className="hidden md:block">Step</span>
</Button>
<Button
className="md:mr-3"
variant="secondary"
onClick={onStepOverBlock}
disabled={
(!isDebugFinished && isRunMode) ||
isDebugFinished ||
!pvmInitialized ||
isProgramEditMode ||
isLoading ||
!!error
}
>
{isLoading ? (
<LoadingSpinner className="w-3.5 md:mr-1.5" size={20} />
) : (
<SkipForward className="w-3.5 md:mr-1.5" />
)}
<span className="hidden md:block">Block</span>
</Button>
{/* TODO fix dark mode */}
{error && <ErrorWarningTooltip variant="light" msg={error} />}
{error && <ErrorWarningTooltip classNames="m-2" variant="light" msg={error} />}
</div>
);
};
17 changes: 14 additions & 3 deletions src/components/DebuggerSettings/Content.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useAppDispatch, useAppSelector } from "@/store/hooks";
import { setServiceId, setSpiArgs, setStepsToPerform } from "@/store/debugger/debuggerSlice";
import { setServiceId, setSpiArgs, setStepsToPerform, setUseBlockStepping } from "@/store/debugger/debuggerSlice";
import { NumeralSystemContext } from "@/context/NumeralSystemContext";
import { valueToNumeralSystem } from "../Instructions/utils";
import { useContext, useState } from "react";
Expand All @@ -11,6 +11,7 @@ import { logger } from "@/utils/loggerService";
import { ToggleDarkMode } from "@/packages/ui-kit/DarkMode/ToggleDarkMode";
import { Separator } from "../ui/separator";
import { WithHelp } from "../WithHelp/WithHelp";
import { Switch } from "@/components/ui/switch";

function stringToNumber<T>(value: string, cb: (x: string) => T): T {
try {
Expand Down Expand Up @@ -94,6 +95,18 @@ export const DebuggerSettingsContent = () => {
value={debuggerState.stepsToPerform}
/>
</div>
<div className="p-4 flex justify-between items-center mb-4">
<span className="block text-xs font-bold">
<WithHelp help="When enabled, Run and Continue commands will step block-by-block instead of instruction-by-instruction. Only available when Batched Steps is not enabled.">
Block Stepping
</WithHelp>
</span>
<Switch
disabled={debuggerState.stepsToPerform > 1}
checked={debuggerState.useBlockStepping}
onCheckedChange={(checked) => dispatch(setUseBlockStepping(checked))}
/>
</div>
<div className="p-4 flex justify-between items-center mb-2">
<span className="block text-xs font-bold">
<WithHelp
Expand All @@ -104,8 +117,6 @@ export const DebuggerSettingsContent = () => {
Host Calls Trace
</WithHelp>
</span>

<div className="flex">TODO</div>
</div>

<div className="p-4 mt-2 flex justify-between items-center mb-4">
Expand Down
128 changes: 128 additions & 0 deletions src/components/Instructions/BasicBlocks/BasicBlockHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { ChevronDown, ChevronRight } from "lucide-react";
import { BasicBlockGroup } from "./blockUtils";
import { forwardRef, useContext, useMemo } from "react";
import { NumeralSystemContext } from "@/context/NumeralSystemContext";
import { NumeralSystem } from "@/context/NumeralSystem";
import { useAppSelector } from "@/store/hooks";
import { selectWorkers } from "@/store/workers/workersSlice";
import classNames from "classnames";
import { useIsDarkMode } from "@/packages/ui-kit/DarkMode/utils";
import { Status } from "@/types/pvm";
import { getStatusColor } from "@/utils/colors";
import { cn, hexToRgb } from "@/lib/utils";

interface BasicBlockHeaderProps {
block: BasicBlockGroup;
isExpanded: boolean;
onToggle: () => void;
status?: Status;
hasBreakpoint?: boolean;
style?: React.CSSProperties;
className?: string;
widestItemValueLength: number;
"data-index"?: number;
}

export const BasicBlockHeader = forwardRef<HTMLTableRowElement, BasicBlockHeaderProps>((props, ref) => {
const { block, isExpanded, onToggle, status, hasBreakpoint = false, style, className, widestItemValueLength } = props;
const { numeralSystem } = useContext(NumeralSystemContext);
const isDarkMode = useIsDarkMode();
const workers = useAppSelector(selectWorkers);
const isHex = numeralSystem === NumeralSystem.HEXADECIMAL;

// Check if any worker's PC is in this block
const workersInBlock = useMemo(() => {
return workers.filter((worker) => {
const pc = worker.currentState.pc;
return pc !== undefined && pc >= block.startAddress && pc <= block.endAddress;
});
}, [workers, block.startAddress, block.endAddress]);

const isActive = workersInBlock.length > 0;
const bgOpacity = workersInBlock.length / Math.max(workers.length, 1);

// Get colors based on status and activity
const colors = useMemo(() => {
if (status === Status.OK && isActive) {
return getStatusColor(isDarkMode);
}
return getStatusColor(isDarkMode, status);
}, [status, isActive, isDarkMode]);

// Format address for display - matching InstructionItem style
const formatAddress = (address: number) => {
const counter = address;
const valInNumeralSystem = isHex ? (counter >>> 0).toString(16) : counter.toString();
const paddingLength = 8 - (isHex ? 2 : 0) - valInNumeralSystem.length;

return (
<div>
{isHex && <span className="text-muted-foreground">0x</span>}
{[...Array(Math.max(0, paddingLength))].map((_, idx) => (
<span key={idx} className="text-muted-foreground">
0
</span>
))}
<span className="text-inherit">{valInNumeralSystem}</span>
</div>
);
};

const blockBackground = isDarkMode
? block.blockNumber % 2 === 0
? "#242424"
: "#2D2D2D"
: block.blockNumber % 2 === 0
? "#fff"
: "#eee";

const backgroundColor = isActive
? `rgba(${hexToRgb(colors.background.toUpperCase())}, ${bgOpacity})`
: blockBackground;

const textColor = isActive ? colors.color : isDarkMode ? "#B3B3B3" : "#14181F";
const borderColor = hasBreakpoint ? "#EF4444" : isActive ? colors.border : isDarkMode ? "#444444" : "#EBEBEB";

return (
<tr
ref={ref}
data-index={props["data-index"]}
className={classNames("cursor-pointer overflow-hidden opacity-75", className)}
onClick={onToggle}
style={{
backgroundColor,
color: textColor,
...style,
}}
>
{/* Address Column with Expand/Collapse Icon */}
<td className="p-1.5 cursor-pointer relative w-[20%] border-b">
<div style={{ backgroundColor: borderColor }} className="w-[3px] absolute h-[100%] left-0 top-0" />
<div className="flex items-center gap-1">{formatAddress(block.startAddress)}</div>
</td>

{/* Block Name Column */}
<td
className={cn("p-1.5 border-b w-[35%] min-w-[160px] lowercase", {
italic: isExpanded,
})}
>
{block.blockName}
{isExpanded ? (
<ChevronDown className="inline h-4 w-4 pb-1" />
) : (
<ChevronRight className="inline h-4 w-4 pb-1" />
)}
</td>

{/* Instruction Count Column */}
<td className="p-1.5 whitespace-nowrap border-b">
<span className="block opacity-75" style={{ width: `${widestItemValueLength}ch` }}>
{block.instructionCount} instructions
</span>
</td>
</tr>
);
});

BasicBlockHeader.displayName = "BasicBlockHeader";
Loading
Loading