Skip to content

Commit 094d8cd

Browse files
oleglazariclaude
andcommitted
fix: step controls handle action-based steps, fix doubled sidebar numbers
StepControls now handles action-based steps (init/malloc/free/done) for heap and conceptual exercises, and no longer requires a StackSim to be present. Sidebar strips number prefix from titles since it already displays the exercise number from the ID. Updated c-06 to explain stack vs heap difference directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c95a702 commit 094d8cd

3 files changed

Lines changed: 68 additions & 34 deletions

File tree

src/components/AppShell/Sidebar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export default function Sidebar() {
178178
const isCompleted = state.completed.has(exId);
179179
const numMatch = exId.match(/(\d+)$/);
180180
const num = numMatch ? numMatch[1].padStart(2, '0') : '??';
181+
const displayTitle = ex.title.replace(/^\d+:\s*/, '');
181182

182183
return (
183184
<button
@@ -187,7 +188,7 @@ export default function Sidebar() {
187188
title={ex.title}
188189
>
189190
<span className="sidebar-exercise-num">{num}</span>
190-
<span className="sidebar-exercise-title">{ex.title}</span>
191+
<span className="sidebar-exercise-title">{displayTitle}</span>
191192
{isCompleted && (
192193
<span className="sidebar-exercise-check">{'\u2713'}</span>
193194
)}

src/components/panels/InputPanel/inputs/StepControls.tsx

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function retAddrInMain(symbols: Record<string, number>): number {
1010
}
1111

1212
export default function StepControls() {
13-
const { state, dispatch, stackSim, currentExercise } = useExerciseContext();
13+
const { state, dispatch, stackSim, heapSim, currentExercise } = useExerciseContext();
1414

1515
if (!currentExercise || !currentExercise.steps) return null;
1616

@@ -26,28 +26,61 @@ export default function StepControls() {
2626

2727
const step = currentExercise.steps[state.stepIndex];
2828
const sim = stackSim.current;
29-
if (!sim) return;
29+
const heap = heapSim.current;
3030

31-
if (step.region === 'ret') {
32-
const ret = retAddrInMain(state.symbols);
33-
sim._writeLE(sim.bufSize + sim.canarySize + sim.ebpSize, ret, sim.retSize);
34-
sim.markRegion(sim.bufSize + sim.canarySize + sim.ebpSize, sim.totalSize);
35-
dispatch({
36-
type: 'LOG',
37-
cls: 'action',
38-
msg: `Calling vuln() \u2014 saving the go-back address <span class="log-addr">${hex8(ret)}</span> so we know where to return`,
39-
});
40-
} else if (step.region === 'ebp') {
41-
sim._writeLE(sim.bufSize + sim.canarySize, 0xbfff0200, sim.ebpSize);
42-
sim.markRegion(sim.bufSize + sim.canarySize, sim.bufSize + sim.canarySize + sim.ebpSize);
43-
dispatch({ type: 'LOG', cls: 'action', msg: step.log[1] });
44-
} else if (step.region === 'buffer') {
45-
for (let i = 0; i < sim.bufSize; i++) sim.memory[i] = 0;
46-
sim.markRegion(0, sim.bufSize);
47-
dispatch({ type: 'LOG', cls: 'action', msg: step.log[1] });
48-
} else if (step.region === 'all') {
49-
sim.clearHighlight();
50-
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
31+
if (step.action) {
32+
switch (step.action) {
33+
case 'malloc':
34+
if (heap && step.size != null) {
35+
const result = heap.malloc(step.size);
36+
if (result && step.name) {
37+
dispatch({ type: 'SET_HEAP_NAME', name: step.name, addr: result.addr });
38+
}
39+
}
40+
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
41+
break;
42+
case 'free':
43+
if (heap && step.name) {
44+
const addr = state.heapNames[step.name];
45+
if (addr !== undefined) {
46+
heap.free(addr);
47+
}
48+
}
49+
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
50+
break;
51+
case 'done':
52+
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
53+
break;
54+
default:
55+
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
56+
break;
57+
}
58+
} else if (step.region) {
59+
if (!sim) {
60+
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
61+
} else if (step.region === 'ret') {
62+
const ret = retAddrInMain(state.symbols);
63+
sim._writeLE(sim.bufSize + sim.canarySize + sim.ebpSize, ret, sim.retSize);
64+
sim.markRegion(sim.bufSize + sim.canarySize + sim.ebpSize, sim.totalSize);
65+
dispatch({
66+
type: 'LOG',
67+
cls: 'action',
68+
msg: `Calling vuln() \u2014 saving the go-back address <span class="log-addr">${hex8(ret)}</span> so we know where to return`,
69+
});
70+
} else if (step.region === 'ebp') {
71+
sim._writeLE(sim.bufSize + sim.canarySize, 0xbfff0200, sim.ebpSize);
72+
sim.markRegion(sim.bufSize + sim.canarySize, sim.bufSize + sim.canarySize + sim.ebpSize);
73+
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
74+
} else if (step.region === 'buffer') {
75+
for (let i = 0; i < sim.bufSize; i++) sim.memory[i] = 0;
76+
sim.markRegion(0, sim.bufSize);
77+
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
78+
} else if (step.region === 'all') {
79+
sim.clearHighlight();
80+
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
81+
} else {
82+
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
83+
}
5184
} else {
5285
dispatch({ type: 'LOG', cls: step.log[0], msg: step.log[1] });
5386
}
@@ -56,7 +89,6 @@ export default function StepControls() {
5689
dispatch({ type: 'INCREMENT_STEP' });
5790
dispatch({ type: 'BUMP_VIZ' });
5891

59-
// Check if exercise is now complete (after incrementing)
6092
if (state.stepIndex + 1 >= currentExercise.steps.length) {
6193
dispatch({ type: 'EXERCISE_COMPLETED', exerciseId: currentExercise.id });
6294
}
@@ -66,14 +98,15 @@ export default function StepControls() {
6698
if (!currentExercise) return;
6799
const sim = stackSim.current;
68100

69-
// Re-create sim
70-
const newSim = new StackSim({
71-
bufSize: currentExercise.bufSize ?? 16,
72-
retAddr: retAddrInMain(state.symbols),
73-
savedEbp: 0xbfff0200,
74-
});
75-
newSim.clearBlank();
76-
stackSim.current = newSim;
101+
if (sim) {
102+
const newSim = new StackSim({
103+
bufSize: currentExercise.bufSize ?? 16,
104+
retAddr: retAddrInMain(state.symbols),
105+
savedEbp: 0xbfff0200,
106+
});
107+
newSim.clearBlank();
108+
stackSim.current = newSim;
109+
}
77110

78111
dispatch({ type: 'SET_STEP_INDEX', index: 0 });
79112
dispatch({ type: 'SET_EXEC_LINE', line: -1 });
@@ -91,7 +124,7 @@ export default function StepControls() {
91124
</button>
92125
{allDone && (
93126
<span style={{ color: 'var(--green)', fontSize: '12px', alignSelf: 'center' }}>
94-
All steps complete \u2713
127+
All steps complete {'\u2713'}
95128
</span>
96129
)}
97130
</div>

src/exercises/unit-intro-c/c-06.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const c06: Exercise = {
2525
steps: [
2626
{
2727
action: 'init',
28-
log: ['info', 'So far we have seen local variables on the stack. But sometimes you need memory that outlives the current function, or whose size you do not know at compile time. That is what the heap is for.'],
28+
log: ['info', 'The stack holds local variables that are automatically created and destroyed with each function call. The heap is a separate memory region for dynamic allocation -- memory you request at runtime with malloc() and release with free(). Stack memory is fast but limited and temporary; heap memory is flexible but you must manage it yourself.'],
2929
},
3030
{
3131
action: 'malloc',

0 commit comments

Comments
 (0)