(&mut self, op: u16, ip: usize, cache: &OpcodeCache, chunk: &SSAChunk, caller_slots: &[Val])
| 867 | |
| 868 | #[inline(never)] |
| 869 | fn exec_make_class(&mut self, op: u16, ip: usize, cache: &OpcodeCache, chunk: &SSAChunk, caller_slots: &[Val]) -> Result<(), VmErr> { |
| 870 | // Operand layout mirrors `class_def_with`: low byte = class chunk index, high byte = base count. |
| 871 | let class_idx = (op & 0xFF) as usize; |
| 872 | let num_bases = (op >> 8) as usize; |
| 873 | // Pop bases first so a misencoded operand fails before we touch the body. |
| 874 | let bases = self.pop_n(num_bases)?; |
| 875 | for &b in &bases { |
| 876 | if !b.is_heap() || !matches!(self.heap.get(b), HeapObj::Class(..)) { |
| 877 | return Err(cold_type("base class must be a class object")); |
| 878 | } |
| 879 | } |
| 880 | let Some(body) = chunk.classes.get(class_idx) else { |
| 881 | return Err(cold_runtime("class index out of range")); |
| 882 | }; |
| 883 | let mut class_slots = self.fill_builtins(&body.names); |
| 884 | // Class bodies can read enclosing module-level names, not only builtins. |
| 885 | let mut injected: Vec<(usize, Val)> = Vec::new(); |
| 886 | for (i, name) in body.names.iter().enumerate() { |
| 887 | let bare = ssa_strip(name); |
| 888 | if class_slots.get(i).is_some_and(|v| v.is_undef()) |
| 889 | && let Some(gv) = self.module_state.get(bare).or_else(|| self.globals.get(bare)).copied() |
| 890 | { |
| 891 | class_slots[i] = gv; |
| 892 | injected.push((i, gv)); |
| 893 | } |
| 894 | } |
| 895 | // Pin caller slots as GC roots so a nested class/function body can't sweep them. |
| 896 | let snap = self.live_slots.len(); |
| 897 | self.live_slots.extend_from_slice(caller_slots); |
| 898 | let exec_result = self.exec(body, &mut class_slots); |
| 899 | self.live_slots.truncate(snap); |
| 900 | exec_result?; |
| 901 | let mut methods: Vec<(String, Val)> = Vec::new(); |
| 902 | for (i, name) in body.names.iter().enumerate() { |
| 903 | if let Some(&v) = class_slots.get(i) |
| 904 | && !v.is_undef() { |
| 905 | // Skip injected module globals, unless the body reassigned the slot. |
| 906 | if injected.iter().any(|&(j, gv)| j == i && v.0 == gv.0) { continue; } |
| 907 | let base = ssa_strip(name); |
| 908 | let is_builtin_shadow = v.is_heap() |
| 909 | && matches!(self.heap.get(v), HeapObj::NativeFn(_)) |
| 910 | && self.globals.get(base).copied() == Some(v); |
| 911 | if is_builtin_shadow { continue; } |
| 912 | if let Some(pos) = methods.iter().position(|(n, _)| n == base) { |
| 913 | methods[pos].1 = v; |
| 914 | } else { |
| 915 | methods.push((base.to_string(), v)); |
| 916 | } |
| 917 | } |
| 918 | } |
| 919 | // Name comes from the StoreName target; skip any decorator `Call`s emitted between. |
| 920 | let fused = cache.fused_ref(); |
| 921 | let mut j = ip; |
| 922 | while matches!(fused.get(j).map(|i| i.opcode), Some(OpCode::Call)) { j += 1; } |
| 923 | let name_str = fused.get(j) |
| 924 | .filter(|i| i.opcode == OpCode::StoreName) |
| 925 | .and_then(|i| chunk.names.get(i.operand as usize)) |
| 926 | .map(|n| ssa_strip(n)) |
no test coverage detected