An execution context stack keeps track of where control should return as functions call other functions. Each call adds a return point; each ordinary return removes the most recent one. Nested calls therefore return in reverse order. section 5.7.4 section 12.9
A small CPU emulator makes this behavior visible. Its stack is an array of addresses, with a stack pointer marking the next available slot. To call a function, it saves the current instruction position and moves to the function’s address. To return, it retrieves the saved position. The example checks for stack underflow and attempts to guard against overflow. section 5.7.4
Listing 5.28 Adding the call() and ret() methods
41 fn call(&mut self, addr: u16) {
42 let sp = self.stack_pointer;
43 let stack = &mut self.stack;
44
45 if sp > stack.len() {
46 panic!("Stack overflow!")
47 }
48
49 stack[sp] = self.position_in_memory as u16; #1
50 self.stack_pointer += 1; #2
51 self.position_in_memory = addr as usize; #3
52 }
53
54 fn ret(&mut self) {
55 if self.stack_pointer == 0 {
56 panic!("Stack underflow");
57 }
58
59 self.stack_pointer -= 1;
60 let call_addr = self.stack[self.stack_pointer]; #4
61 self.position_in_memory = call_addr as usize; #4
62 }
Compare call() and ret(): one saves the instruction position and advances the stack pointer; the other reverses those steps. section 5.7.4
Watch nested calls grow and shrink
A recursive function prints a row of # characters before calling itself and again after returning. The rows grow longer with each nested call, then shorten one step at a time as calls return. section 12.9
Normal return is not the only way out. Stack unwinding traverses accumulated frames in reverse during exception handling. On UNIX-based systems, setjmp and longjmp can instead transfer control past several nested calls to a previously marked location. This call-tracking role is related to, but distinct from, choosing whether program data lives on the stack or heap. section 11.4.1 section 12.9 section 6.3







