@@ -863,7 +863,7 @@ impl Stack {
863863 let Some ( ( ip, start, instance) ) = self . frames . restore_frame ( ) else {
864864 panic ! ( "restore_frame: missing top-frame" )
865865 } ;
866- let sp = self . values . sp_or_dangling ( start) ;
866+ let sp = self . values . sp ( start) ;
867867 ( ip, sp, instance, self . ireg , self . freg32 , self . freg64 )
868868 }
869869
@@ -891,22 +891,47 @@ impl Stack {
891891 . prepare_host_frame ( caller_start, callee_params, results_len)
892892 }
893893
894+ /// Returns an [`Sp`] pointing at the base of the value stack.
895+ ///
896+ /// # Note
897+ ///
898+ /// Only valid as the `caller_sp` of the very first frame pushed onto an empty [`Stack`].
899+ pub fn base_sp ( & mut self ) -> Sp {
900+ self . values . base_sp ( )
901+ }
902+
894903 /// Adjusts `self` for a normal function call.
904+ ///
905+ /// # Note
906+ ///
907+ /// `caller_sp` must be the [`Sp`] of the currently executing frame. The callee's [`Sp`]
908+ /// is derived from it by pointer arithmetic instead of being re-loaded from the frame
909+ /// table, which keeps it off the call's dependent-load chain.
895910 #[ inline( always) ]
911+ #[ expect( clippy:: too_many_arguments) ]
896912 pub fn push_frame (
897913 & mut self ,
914+ caller_sp : Sp ,
898915 caller_ip : Option < Ip > ,
899916 callee_ip : Ip ,
900917 callee_params : BoundedSlotSpan ,
901918 callee_locals : u16 ,
902919 callee_slots : u16 ,
903920 callee_instance : Option < Inst > ,
904921 ) -> Result < Sp , TrapCode > {
922+ // Note: the callee's frame starts with its first parameter and its stack pointer
923+ // can be inferred from its parameter slot span and its caller's stack pointer.
924+ let callee_sp = caller_sp. offset ( callee_params. span ( ) . head ( ) ) ;
905925 let start = self
906926 . frames
907927 . push ( caller_ip, callee_ip, callee_params, callee_instance) ?;
908- self . values
909- . push ( start, callee_locals, callee_slots, callee_params. len ( ) )
928+ self . values . push (
929+ callee_sp,
930+ start,
931+ callee_locals,
932+ callee_slots,
933+ callee_params. len ( ) ,
934+ )
910935 }
911936
912937 /// Adjusts `self` after returning from a function.
@@ -926,7 +951,7 @@ impl Stack {
926951 instance : Inst ,
927952 ) -> Option < ( Ip , Sp , Mem0Ptr , Mem0Len , Inst ) > {
928953 let ( ip, start, changed_instance) = self . frames . pop ( ) ?;
929- let sp = self . values . sp_or_dangling ( start) ;
954+ let sp = self . values . sp ( start) ;
930955 let ( mem0, mem0_len, instance) = match changed_instance {
931956 Some ( instance) => {
932957 let ( mem0, mem0_len) = extract_mem0 ( store, instance) ;
@@ -938,18 +963,24 @@ impl Stack {
938963 }
939964
940965 /// Adjusts `self` for a function tail call.
966+ ///
967+ /// # Note
968+ ///
969+ /// A tail call reuses the caller's frame, so the callee's [`Sp`] is `caller_sp` itself.
970+ /// See [`Stack::push_frame`] for why it is threaded through instead of re-loaded.
941971 #[ inline( always) ]
942972 pub fn replace_frame (
943973 & mut self ,
974+ caller_sp : Sp ,
944975 callee_ip : Ip ,
945976 callee_params : BoundedSlotSpan ,
946977 callee_locals : u16 ,
947- callee_size : u16 ,
978+ callee_slots : u16 ,
948979 callee_instance : Option < Inst > ,
949980 ) -> Result < Sp , TrapCode > {
950981 let start = self . frames . replace ( callee_ip, callee_instance) ?;
951982 self . values
952- . replace ( start, callee_locals, callee_size , callee_params)
983+ . replace ( caller_sp , start, callee_locals, callee_slots , callee_params)
953984 }
954985}
955986
@@ -968,6 +999,14 @@ impl Stack {
968999pub struct ValueStack {
9691000 /// The cells of the value stack.
9701001 cells : Vec < Cell > ,
1002+ /// The number of cells that fit without reallocating, capped by [`Self::max_height`].
1003+ ///
1004+ /// # Note
1005+ ///
1006+ /// Kept in sync with `cells` so that the hot path of [`ValueStack::grow_if_needed`]
1007+ /// is a single comparison: `Vec::try_reserve` may over-allocate past `max_height`,
1008+ /// so `cells.capacity()` alone is not a sound bound to test against.
1009+ usable : usize ,
9711010 /// The maximum height of the value stack.
9721011 max_height : usize ,
9731012}
@@ -981,13 +1020,19 @@ impl ValueStack {
9811020 let min_height = min_height / sizeof_cell;
9821021 let max_height = max_height / sizeof_cell;
9831022 let cells = Vec :: with_capacity ( min_height) ;
984- Self { cells, max_height }
1023+ let usable = cmp:: min ( cells. capacity ( ) , max_height) ;
1024+ Self {
1025+ cells,
1026+ usable,
1027+ max_height,
1028+ }
9851029 }
9861030
9871031 /// Create an empty [`ValueStack`] which uses no heap allocations.
9881032 fn empty ( ) -> Self {
9891033 Self {
9901034 cells : Vec :: new ( ) ,
1035+ usable : 0 ,
9911036 max_height : 0 ,
9921037 }
9931038 }
@@ -997,6 +1042,17 @@ impl ValueStack {
9971042 self . cells . clear ( ) ;
9981043 }
9991044
1045+ /// Returns an [`Sp`] pointing at the base of the value stack.
1046+ ///
1047+ /// # Note
1048+ ///
1049+ /// Only used to seed the very first frame, where `start` is 0. If the buffer has
1050+ /// not been allocated yet the returned [`Sp`] is dangling, but pushing that frame
1051+ /// then reallocates and re-derives it.
1052+ fn base_sp ( & mut self ) -> Sp {
1053+ Sp :: new ( self . cells . as_mut_ptr ( ) )
1054+ }
1055+
10001056 /// Returns the number of heap allocated bytes of `self`.
10011057 ///
10021058 /// # Note
@@ -1008,6 +1064,12 @@ impl ValueStack {
10081064 }
10091065
10101066 /// Returns an [`Sp`] pointing to the cell at the `start` index.
1067+ ///
1068+ /// # Note
1069+ ///
1070+ /// This is the single definition of a frame's [`Sp`]: every one of them equals
1071+ /// `cells.as_ptr().add(start)`, including on an empty stack where `start` is 0 and the
1072+ /// result is the (never dereferenced) base pointer.
10111073 fn sp ( & mut self , start : SpOffset ) -> Sp {
10121074 let offset = start. into_inner ( ) ;
10131075 debug_assert ! (
@@ -1023,39 +1085,56 @@ impl ValueStack {
10231085 Sp :: new ( value)
10241086 }
10251087
1026- /// Returns an [`Sp`] pointing to the cell at the `start` index if `self` is non-empty.
1027- ///
1028- /// Otherwise returns a dangling [`Sp`] that must not be dereferenced.
1029- fn sp_or_dangling ( & mut self , start : SpOffset ) -> Sp {
1030- match self . cells . is_empty ( ) {
1031- true => {
1032- debug_assert_eq ! ( start. into_inner( ) , 0 ) ;
1033- Sp :: dangling ( )
1034- }
1035- false => self . sp ( start) ,
1036- }
1037- }
1038-
10391088 /// Grows the number of cells to `new_len` if the current number is less than `new_len`.
10401089 ///
10411090 /// Does nothing if the number of cells is already at least `new_len`.
10421091 ///
1092+ /// # Returns
1093+ ///
1094+ /// `true` if the underlying buffer was reallocated, which invalidates every [`Sp`]
1095+ /// derived from it before the call.
1096+ ///
10431097 /// # Errors
10441098 ///
10451099 /// - Returns [`TrapCode::OutOfSystemMemory`] if the machine ran out of memory.
10461100 /// - Returns [`TrapCode::StackOverflow`] if this exceeds the stack's predefined limits.
1047- fn grow_if_needed ( & mut self , new_len : SpOffset ) -> Result < ( ) , TrapCode > {
1101+ #[ inline( always) ]
1102+ fn grow_if_needed ( & mut self , new_len : SpOffset ) -> Result < bool , TrapCode > {
10481103 let new_len = new_len. into_inner ( ) ;
1104+ if new_len > self . usable {
1105+ self . grow_cold ( new_len) ?;
1106+ return Ok ( true ) ;
1107+ }
1108+ if new_len > self . cells . len ( ) {
1109+ // Safety: `new_len <= self.usable <= self.cells.capacity()`. There is no need to
1110+ // initialize the cells since we are operating on `Cell` which only has
1111+ // valid bit patterns.
1112+ // Note: non-security related initialization of function parameters
1113+ // and zero-initialization of function locals happens elsewhere.
1114+ unsafe { self . cells . set_len ( new_len) } ;
1115+ }
1116+ Ok ( false )
1117+ }
1118+
1119+ /// Reallocates the value stack so that it holds at least `new_len` cells.
1120+ ///
1121+ /// # Note
1122+ ///
1123+ /// Split out of [`ValueStack::grow_if_needed`] and marked `#[cold]` so that the
1124+ /// common no-growth path stays a single comparison and keeps its caller's [`Sp`]
1125+ /// valid in a register.
1126+ #[ cold]
1127+ #[ inline( never) ]
1128+ fn grow_cold ( & mut self , new_len : usize ) -> Result < ( ) , TrapCode > {
10491129 if new_len > self . max_height {
10501130 return Err ( TrapCode :: StackOverflow ) ;
10511131 }
1052- let capacity = self . cells . capacity ( ) ;
10531132 let len = self . cells . len ( ) ;
1054- if new_len > capacity {
1055- debug_assert ! (
1056- self . cells . len ( ) <= self . cells. capacity ( ) ,
1057- "capacity must always be larger or equal to the actual number of the cells"
1058- ) ;
1133+ debug_assert ! (
1134+ len <= self . cells . capacity ( ) ,
1135+ "capacity must always be larger or equal to the actual number of the cells"
1136+ ) ;
1137+ if new_len > self . cells . capacity ( ) {
10591138 let additional = new_len - len;
10601139 self . cells
10611140 . try_reserve ( additional)
@@ -1066,12 +1145,9 @@ impl ValueStack {
10661145 self . cells. capacity( )
10671146 ) ;
10681147 }
1069- let max_len = cmp:: max ( new_len, len) ;
1070- // Safety: there is no need to initialize the cells since we are operating
1071- // on `RawVal` which only has valid bit patterns.
1072- // Note: non-security related initialization of function parameters
1073- // and zero-initialization of function locals happens elsewhere.
1074- unsafe { self . cells . set_len ( max_len) } ;
1148+ self . usable = cmp:: min ( self . cells . capacity ( ) , self . max_height ) ;
1149+ // Safety: see `grow_if_needed`.
1150+ unsafe { self . cells . set_len ( cmp:: max ( new_len, len) ) } ;
10751151 Ok ( ( ) )
10761152 }
10771153
@@ -1171,6 +1247,7 @@ impl ValueStack {
11711247 #[ inline( always) ]
11721248 fn push (
11731249 & mut self ,
1250+ callee_sp : Sp ,
11741251 start : SpOffset ,
11751252 len_local_slots : u16 ,
11761253 len_stack_slots : u16 ,
@@ -1195,40 +1272,55 @@ impl ValueStack {
11951272 let len_stack_slots = usize:: from ( len_stack_slots) ;
11961273 let len_params = usize:: from ( len_params) ;
11971274 if len_stack_slots == 0 {
1198- return Ok ( Sp :: dangling ( ) ) ;
1275+ // Note: a callee without stack slots also has no parameters, so the translator
1276+ // encodes a zero `params` head and `callee_sp == caller_sp`. Propagate it
1277+ // rather than a fresh dangling pointer, so that any call this frame goes on
1278+ // to make can still derive its callee's `Sp` from this one.
1279+ return Ok ( callee_sp) ;
11991280 }
12001281 let end = start. add ( len_stack_slots) ?;
1201- self . grow_if_needed ( end) ?;
1282+ // Note: `callee_sp` was derived from the caller's `Sp` register, so it only survives
1283+ // if the buffer was not reallocated. Re-deriving is confined to the cold path.
1284+ let sp = match self . grow_if_needed ( end) ? {
1285+ true => self . sp ( start) ,
1286+ false => callee_sp,
1287+ } ;
12021288 let start_locals = start. into_inner ( ) . wrapping_add ( len_params) ;
12031289 let end_locals = start. into_inner ( ) . wrapping_add ( len_local_slots) ;
12041290 let Some ( local_cells) = self . cells . get_mut ( start_locals..end_locals) else {
12051291 unsafe { unreachable_unchecked ! ( ) }
12061292 } ;
12071293 local_cells. fill_with ( Cell :: default) ;
1208- let sp = self . sp ( start) ;
12091294 Ok ( sp)
12101295 }
12111296
12121297 /// Adjusts `self` for a function tail call.
12131298 #[ inline( always) ]
12141299 fn replace (
12151300 & mut self ,
1301+ callee_sp : Sp ,
12161302 callee_start : SpOffset ,
12171303 callee_locals : u16 ,
1218- callee_size : u16 ,
1304+ callee_slots : u16 ,
12191305 callee_params : BoundedSlotSpan ,
12201306 ) -> Result < Sp , TrapCode > {
1221- debug_assert ! ( callee_locals <= callee_size ) ;
1307+ debug_assert ! ( callee_locals <= callee_slots ) ;
12221308 let callee_locals = usize:: from ( callee_locals) ;
1223- let callee_size = usize:: from ( callee_size ) ;
1309+ let callee_size = usize:: from ( callee_slots ) ;
12241310 let params_len = usize:: from ( callee_params. len ( ) ) ;
12251311 let params_start = usize:: from ( u16:: from ( callee_params. span ( ) . head ( ) ) ) ;
12261312 let params_end = params_start. wrapping_add ( params_len) ;
12271313 if callee_size == 0 {
1228- return Ok ( Sp :: dangling ( ) ) ;
1314+ // Note: see `ValueStack::push` — the frame is reused, so this is the caller's `Sp`.
1315+ return Ok ( callee_sp) ;
12291316 }
12301317 let callee_end = callee_start. add ( callee_size) ?;
1231- self . grow_if_needed ( callee_end) ?;
1318+ // Note: a tail call reuses the caller's frame, so `callee_sp` is the caller's `Sp`
1319+ // unchanged and only needs re-deriving if the buffer was reallocated.
1320+ let sp = match self . grow_if_needed ( callee_end) ? {
1321+ true => self . sp ( callee_start) ,
1322+ false => callee_sp,
1323+ } ;
12321324 let Some ( callee_cells) = self . cells_from ( callee_start) else {
12331325 unsafe { unreachable_unchecked ! ( "ValueStack::replace: out of bounds callee cells" ) }
12341326 } ;
@@ -1237,7 +1329,6 @@ impl ValueStack {
12371329 unsafe { unreachable_unchecked ! ( ) }
12381330 } ;
12391331 local_cells. fill_with ( Cell :: default) ;
1240- let sp = self . sp ( callee_start) ;
12411332 Ok ( sp)
12421333 }
12431334
0 commit comments