//! Toolbox Utility trap handlers (events, Random, Sound, misc).
use crate::cpu::{CpuOps, Register};
use crate::memory::globals::addr;
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::{Error, Result};
use std::sync::OnceLock;
static TRACE_MUNGER: OnceLock<bool> = OnceLock::new();
static TRACE_LIST: OnceLock<bool> = OnceLock::new();
static TRACE_ENTROPY: OnceLock<bool> = OnceLock::new();
static TRACE_TITLE_DIAG: OnceLock<bool> = OnceLock::new();
static TRACE_SOUND: OnceLock<bool> = OnceLock::new();
static FORCE_BUTTON_TRUE_AT_PC: OnceLock<Option<u32>> = OnceLock::new();
fn standard_file_cancel_reply(bus: &mut MacMemoryBus, reply_ptr: u32) {
if reply_ptr != 0 {
// SFReply and StandardFileReply both start with the cancel flag.
bus.write_byte(reply_ptr, 0);
}
}
#[inline]
fn return_noerr_and_pop<C: CpuOps>(cpu: &mut C, bytes: u32) -> Result<()> {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp.wrapping_add(bytes));
cpu.write_reg(Register::D0, 0);
Ok(())
}
#[inline]
fn return_noerr<C: CpuOps>(cpu: &mut C) -> Result<()> {
cpu.write_reg(Register::D0, 0);
Ok(())
}
#[inline]
fn return_error_and_pop<C: CpuOps>(cpu: &mut C, bytes: u32, err: i16) -> Result<()> {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp.wrapping_add(bytes));
cpu.write_reg(Register::D0, err as u32);
Ok(())
}
/// Returns true if `year` is a leap year in the Gregorian calendar.
fn is_leap_year(year: u32) -> bool {
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}
/// Days in each month (index 1-based). February is 28; caller must add 1 for leap years.
const DAYS_IN_MONTH: [u32; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
/// Convert seconds since Mac epoch (Jan 1, 1904 00:00:00) to DateTimeRec fields.
/// Returns (year, month, day, hour, minute, second, dayOfWeek).
/// dayOfWeek: 1=Sunday..7=Saturday.
/// Inside Macintosh Volume II, II-379
fn secs_to_date(secs: u32) -> (u16, u16, u16, u16, u16, u16, u16) {
// Jan 1, 1904 was a Friday = dayOfWeek 6
let day_of_week = ((secs / 86400 + 5) % 7 + 1) as u16; // +5 because Jan 1 1904 = Friday (6), Sunday=1
let mut remaining = secs;
let second = (remaining % 60) as u16;
remaining /= 60;
let minute = (remaining % 60) as u16;
remaining /= 60;
let hour = (remaining % 24) as u16;
let mut days = remaining / 24;
let mut year = 1904u32;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if days < days_in_year {
break;
}
days -= days_in_year;
year += 1;
}
let mut month = 1u32;
loop {
let mut dim = DAYS_IN_MONTH[month as usize];
if month == 2 && is_leap_year(year) {
dim += 1;
}
if days < dim {
break;
}
days -= dim;
month += 1;
}
let day = days + 1; // 1-based
(
year as u16,
month as u16,
day as u16,
hour,
minute,
second,
day_of_week,
)
}
/// Convert DateTimeRec fields to seconds since Mac epoch (Jan 1, 1904 00:00:00).
/// Inside Macintosh Volume II, II-379
fn date_to_secs(year: u32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> u32 {
let mut days: u32 = 0;
for y in 1904..year {
days += if is_leap_year(y) { 366 } else { 365 };
}
for m in 1..month {
days += DAYS_IN_MONTH[m as usize];
if m == 2 && is_leap_year(year) {
days += 1;
}
}
days += day - 1; // day is 1-based
days * 86400 + hour * 3600 + minute * 60 + second
}
fn trace_munger_enabled() -> bool {
*TRACE_MUNGER.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_MUNGER").is_some())
}
fn trace_list_manager_enabled() -> bool {
*TRACE_LIST.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_LIST").is_some())
}
fn trace_entropy_enabled() -> bool {
*TRACE_ENTROPY.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_ENTROPY").is_some())
}
fn trace_title_diag_enabled() -> bool {
*TRACE_TITLE_DIAG.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_TITLE_DIAG").is_some())
}
fn trace_sound_enabled() -> bool {
*TRACE_SOUND.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_SOUND").is_some())
}
fn force_button_true_at_pc() -> Option<u32> {
*FORCE_BUTTON_TRUE_AT_PC.get_or_init(|| {
let s = std::env::var("SYSTEMLESS_FORCE_BUTTON_TRUE_AT_PC").ok()?;
let s = s.strip_prefix("0x").unwrap_or(&s);
u32::from_str_radix(s, 16).ok()
})
}
impl super::TrapDispatcher {
const LIST_RVIEW_OFFSET: u32 = 0;
const LIST_PORT_OFFSET: u32 = 8;
const LIST_INDENT_OFFSET: u32 = 12;
const LIST_CELL_SIZE_OFFSET: u32 = 16;
const LIST_VISIBLE_OFFSET: u32 = 20;
const LIST_VSCROLL_OFFSET: u32 = 28;
const LIST_HSCROLL_OFFSET: u32 = 32;
const LIST_SEL_FLAGS_OFFSET: u32 = 36;
const LIST_ACTIVE_OFFSET: u32 = 37;
const LIST_RESERVED_OFFSET: u32 = 38;
const LIST_FLAGS_OFFSET: u32 = 39;
const LIST_CLICK_TIME_OFFSET: u32 = 40;
const LIST_CLICK_LOC_OFFSET: u32 = 44;
const LIST_MOUSE_LOC_OFFSET: u32 = 48;
const LIST_CLICK_LOOP_OFFSET: u32 = 52;
const LIST_LAST_CLICK_OFFSET: u32 = 56;
const LIST_REFCON_OFFSET: u32 = 60;
const LIST_DEF_PROC_OFFSET: u32 = 64;
const LIST_USER_HANDLE_OFFSET: u32 = 68;
const LIST_DATA_BOUNDS_OFFSET: u32 = 72;
const LIST_CELLS_OFFSET: u32 = 80;
const LIST_MAX_INDEX_OFFSET: u32 = 84;
const LIST_CELL_ARRAY_OFFSET: u32 = 86;
const LIST_RECORD_SIZE: u32 = 88;
const LIST_DOUBLE_CLICK_TICKS: u32 = 20;
fn stack_bool_slot(bus: &MacMemoryBus, addr: u32) -> bool {
// MPW Pascal callers store BOOLEAN in the high byte of the
// 2-byte stack slot. The low byte is padding and can retain
// unrelated non-zero garbage, so only the first byte is
// semantically meaningful.
bus.read_byte(addr) != 0
}
fn read_stack_point(bus: &MacMemoryBus, addr: u32) -> (i16, i16) {
(bus.read_word(addr) as i16, bus.read_word(addr + 2) as i16)
}
fn read_rect_ptr(bus: &MacMemoryBus, ptr: u32) -> (i16, i16, i16, i16) {
(
bus.read_word(ptr) as i16,
bus.read_word(ptr + 2) as i16,
bus.read_word(ptr + 4) as i16,
bus.read_word(ptr + 6) as i16,
)
}
fn write_rect_words(bus: &mut MacMemoryBus, addr: u32, rect: (i16, i16, i16, i16)) {
bus.write_word(addr, rect.0 as u16);
bus.write_word(addr + 2, rect.1 as u16);
bus.write_word(addr + 4, rect.2 as u16);
bus.write_word(addr + 6, rect.3 as u16);
}
fn write_point_words(bus: &mut MacMemoryBus, addr: u32, point: (i16, i16)) {
bus.write_word(addr, point.0 as u16);
bus.write_word(addr + 2, point.1 as u16);
}
fn list_no_click_cell() -> (i16, i16) {
(-1, -1)
}
fn serialized_scrap_size(&self) -> u32 {
self.scrap_entries
.iter()
.map(|(_, data)| {
let padded = (data.len() as u32 + 1) & !1;
8 + padded // type(4) + length(4) + padded data
})
.sum()
}
fn serialize_scrap_entries(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.serialized_scrap_size() as usize);
for (entry_type, data) in &self.scrap_entries {
bytes.extend_from_slice(entry_type);
bytes.extend_from_slice(&(data.len() as u32).to_be_bytes());
bytes.extend_from_slice(data);
if (data.len() & 1) != 0 {
bytes.push(0);
}
}
bytes
}
/// Copy `bytes` into `handle`, resizing or replacing its backing
/// allocation as needed and keeping the handle-ownership map in sync.
/// Returns the current master-pointer target, or 0 for an empty handle
/// or allocation failure.
fn write_bytes_to_handle(&mut self, bus: &mut MacMemoryBus, handle: u32, bytes: &[u8]) -> u32 {
if handle == 0 {
return 0;
}
let new_size = bytes.len() as u32;
let old_ptr = bus.read_long(handle);
if new_size == 0 {
if old_ptr != 0 {
bus.free(old_ptr);
self.ptr_to_handle.remove(&old_ptr);
}
if let Some(entry) = self.loaded_handles.get_mut(&handle) {
entry.0 = 0;
}
bus.write_long(handle, 0);
return 0;
}
if old_ptr != 0 {
let old_size = bus.get_alloc_size(old_ptr).unwrap_or(0);
let aligned_old = (old_size + 3) & !3;
let aligned_new = (new_size + 3) & !3;
if old_size == new_size || aligned_new <= aligned_old {
bus.set_alloc_size(old_ptr, new_size);
bus.write_bytes(old_ptr, bytes);
self.ptr_to_handle.insert(old_ptr, handle);
if let Some(entry) = self.loaded_handles.get_mut(&handle) {
entry.0 = old_ptr;
}
return old_ptr;
}
}
let new_ptr = bus.alloc(new_size);
if new_ptr == 0 {
return 0;
}
bus.write_bytes(new_ptr, bytes);
if old_ptr != 0 {
bus.free(old_ptr);
self.ptr_to_handle.remove(&old_ptr);
}
bus.write_long(handle, new_ptr);
self.ptr_to_handle.insert(new_ptr, handle);
if let Some(entry) = self.loaded_handles.get_mut(&handle) {
entry.0 = new_ptr;
}
new_ptr
}
fn sync_scrap_handle(&mut self, bus: &mut MacMemoryBus) -> u32 {
let handle = *self.scrap_handle.get_or_insert_with(|| {
let handle = bus.alloc(4);
if handle != 0 {
bus.write_long(handle, 0);
}
handle
});
if handle == 0 {
return 0;
}
if !self.scrap_handle_dirty {
return handle;
}
let bytes = self.serialize_scrap_entries();
let wrote = if bytes.is_empty() {
self.write_bytes_to_handle(bus, handle, &bytes);
true
} else {
self.write_bytes_to_handle(bus, handle, &bytes) != 0
};
if wrote {
self.scrap_handle_dirty = false;
}
handle
}
fn list_record_ptr(bus: &MacMemoryBus, list_handle: u32) -> u32 {
if list_handle == 0 {
0
} else {
bus.read_long(list_handle)
}
}
fn compute_list_cell_size(
&self,
view_rect: (i16, i16, i16, i16),
data_bounds: (i16, i16, i16, i16),
requested: (i16, i16),
) -> (i16, i16) {
let cols = (data_bounds.3 - data_bounds.1).max(1);
let default_h = ((view_rect.3 - view_rect.1).max(1) / cols).max(1);
let default_v = self.tx_size.max(9) + 2;
let cell_v = if requested.0 > 0 {
requested.0
} else {
default_v
};
let cell_h = if requested.1 > 0 {
requested.1
} else {
default_h
};
(cell_v.max(1), cell_h.max(1))
}
fn compute_list_visible_rect(
view_rect: (i16, i16, i16, i16),
data_bounds: (i16, i16, i16, i16),
cell_size: (i16, i16),
) -> (i16, i16, i16, i16) {
let rows_visible = ((view_rect.2 - view_rect.0).max(0) + cell_size.0 - 1) / cell_size.0;
let cols_visible = ((view_rect.3 - view_rect.1).max(0) + cell_size.1 - 1) / cell_size.1;
(
data_bounds.0,
data_bounds.1,
(data_bounds.0 + rows_visible).min(data_bounds.2),
(data_bounds.1 + cols_visible).min(data_bounds.3),
)
}
fn sync_list_state_to_guest(
bus: &mut MacMemoryBus,
list_handle: u32,
state: &super::dispatch::ListState,
) {
let list_ptr = Self::list_record_ptr(bus, list_handle);
if list_ptr == 0 {
return;
}
Self::write_rect_words(bus, list_ptr + Self::LIST_RVIEW_OFFSET, state.view_rect);
bus.write_long(list_ptr + Self::LIST_PORT_OFFSET, state.port);
Self::write_point_words(bus, list_ptr + Self::LIST_INDENT_OFFSET, (0, 0));
Self::write_point_words(bus, list_ptr + Self::LIST_CELL_SIZE_OFFSET, state.cell_size);
Self::write_rect_words(bus, list_ptr + Self::LIST_VISIBLE_OFFSET, state.visible);
Self::write_point_words(
bus,
list_ptr + Self::LIST_LAST_CLICK_OFFSET,
state.last_click,
);
Self::write_rect_words(
bus,
list_ptr + Self::LIST_DATA_BOUNDS_OFFSET,
state.data_bounds,
);
let rows = (state.data_bounds.2 - state.data_bounds.0).max(0) as i32;
let cols = (state.data_bounds.3 - state.data_bounds.1).max(0) as i32;
bus.write_word(
list_ptr + Self::LIST_MAX_INDEX_OFFSET,
rows.saturating_mul(cols).saturating_mul(2) as u16,
);
}
fn list_cell_is_valid(state: &super::dispatch::ListState, row: i16, col: i16) -> bool {
row >= state.data_bounds.0
&& row < state.data_bounds.2
&& col >= state.data_bounds.1
&& col < state.data_bounds.3
}
fn list_cell_from_point(
state: &super::dispatch::ListState,
point: (i16, i16),
) -> Option<(i16, i16)> {
let (pt_v, pt_h) = point;
let view = state.view_rect;
if pt_v < view.0 || pt_v >= view.2 || pt_h < view.1 || pt_h >= view.3 {
return None;
}
let row = state.visible.0 + ((pt_v - view.0) / state.cell_size.0.max(1));
let col = state.visible.1 + ((pt_h - view.1) / state.cell_size.1.max(1));
if Self::list_cell_is_valid(state, row, col) {
Some((row, col))
} else {
None
}
}
fn scsi_dispatch_arg_bytes(selector: i16) -> u32 {
match selector {
0 | 1 | 10 => 0, // SCSIReset, SCSIGet, SCSIStat
2 | 11 | 13 => 2, // SCSISelect, SCSISelAtn, SCSIMsgOut
3 => 6, // SCSICmd(buffer, count)
4 => 12, // SCSIComplete(stat, message, wait)
5 | 6 | 7 | 8 | 9 | 12 => 4, // tibPtr/sihPtr/message ptr
_ => 0,
}
}
fn pack0_fallback<C: CpuOps>(
&mut self,
cpu: &mut C,
bus: &mut MacMemoryBus,
sp: u32,
selector: u16,
) -> Result<()> {
let (param_bytes, result_bytes) = match selector {
0x00 => (6, 0), // LActivate
0x04 => (8, 2), // LAddColumn
0x08 => (8, 2), // LAddRow
0x0C => (14, 0), // LAddToCell
0x10 => (4, 0), // LAutoScroll
0x14 => (8, 0), // LCellSize
0x18 => (10, 2), // LClick
0x1C => (8, 0), // LClrCell
0x20 => (8, 0), // LDelColumn
0x24 => (8, 0), // LDelRow
0x28 => (4, 0), // LDispose
0x2C => (6, 0), // LDoDraw
0x30 => (8, 0), // LDraw
0x34 => (8, 4), // LFind
0x38 => (16, 0), // LGetCell
0x3C => (10, 2), // LGetSelect
0x40 => (4, 4), // LLastClick
0x44 => (26, 4), // LNew
0x48 => (10, 2), // LNextCell
0x4C => (12, 0), // LRect
0x50 => (8, 0), // LScroll
0x54 => (16, 2), // LSearch
0x58 => (14, 0), // LSetCell
0x5C => (10, 0), // LSetSelect
0x60 => (8, 0), // LSize
0x64 => (8, 0), // LUpdate
_ => {
eprintln!("[LIST] Unimplemented selector ${:04X}", selector);
return Err(Error::Halted);
}
};
let result_addr = sp + 2 + param_bytes;
if result_bytes == 2 {
bus.write_word(result_addr, 0);
} else if result_bytes == 4 {
bus.write_long(result_addr, 0);
}
cpu.write_reg(Register::A7, result_addr);
Ok(())
}
fn munger_in_handle(
bus: &mut MacMemoryBus,
trap_site: u32,
handle: u32,
offset: i32,
ptr1: u32,
len1: i32,
ptr2: u32,
len2: i32,
) -> i32 {
if handle == 0 || offset < 0 {
return -1;
}
let data_ptr = bus.read_long(handle);
let old_size = data_ptr
.checked_sub(0)
.and_then(|_| bus.get_alloc_size(data_ptr))
.unwrap_or(0) as usize;
let data = if old_size > 0 {
bus.read_bytes(data_ptr, old_size)
} else {
Vec::new()
};
let needle = if ptr1 != 0 && len1 > 0 {
bus.read_bytes(ptr1, len1 as usize)
} else {
Vec::new()
};
let replacement = if ptr2 != 0 && len2 > 0 {
bus.read_bytes(ptr2, len2 as usize)
} else {
Vec::new()
};
let should_trace = trace_munger_enabled();
let offset = offset as usize;
if offset > data.len() {
return -1;
}
let mut replace_offset = offset;
let mut replace_len = len1.max(0) as usize;
if ptr1 != 0 && len1 > 0 {
let mut search = offset;
let mut found = None;
while search < data.len() {
let remaining = data.len() - search;
let compare_len = needle.len().min(remaining);
if compare_len > 0 && data[search..search + compare_len] == needle[..compare_len] {
found = Some((search, compare_len == needle.len()));
break;
}
search += 1;
}
let Some((found_offset, full_match)) = found else {
return -1;
};
replace_offset = found_offset;
if full_match {
replace_len = needle.len();
} else {
// BasiliskII/System 7.5 ROM does not perform the Apple-
// documented tail-partial replacement here; it treats the
// partial tail match as not found and leaves the destination
// bytes unchanged.
return -1;
}
} else if ptr1 == 0 && len1 < 0 {
replace_len = data.len() - offset;
}
replace_len = replace_len.min(data.len().saturating_sub(replace_offset));
if ptr2 == 0 && ptr1 != 0 {
if should_trace {
eprintln!(
"[MUNGER] @${:08X} h=${:08X} ptr=${:08X} old_size={} offset={} len1={} len2={} needle={:02X?} replacement=<search-only> before={:02X?} result={}",
trap_site,
handle,
data_ptr,
old_size,
offset,
len1,
len2,
needle,
data,
replace_offset
);
}
return replace_offset as i32;
}
let tail_start = replace_offset + replace_len;
let mut new_data = Vec::with_capacity(data.len() - replace_len + replacement.len());
new_data.extend_from_slice(&data[..replace_offset]);
new_data.extend_from_slice(&replacement);
new_data.extend_from_slice(&data[tail_start..]);
if new_data.is_empty() {
if data_ptr != 0 {
bus.free(data_ptr);
}
bus.write_long(handle, 0);
} else if data_ptr == 0
|| bus.get_alloc_size(data_ptr).unwrap_or(0) != new_data.len() as u32
{
let new_ptr = bus.alloc(new_data.len() as u32);
if new_ptr == 0 {
return -1;
}
bus.write_bytes(new_ptr, &new_data);
if data_ptr != 0 {
bus.free(data_ptr);
}
bus.write_long(handle, new_ptr);
} else {
bus.write_bytes(data_ptr, &new_data);
}
let result = (replace_offset + replacement.len()) as i32;
if should_trace {
eprintln!(
"[MUNGER] @${:08X} h=${:08X} ptr=${:08X} old_size={} offset={} len1={} len2={} needle={:02X?} replacement={:02X?} before={:02X?} after={:02X?} result={}",
trap_site,
handle,
data_ptr,
old_size,
offset,
len1,
len2,
needle,
replacement,
data,
new_data,
result
);
}
result
}
/// Minimal KeyTranslate / KeyTrans helper for the nominal
/// non-dead-key path.
///
/// The caller supplies a pointer to a `'KCHR'` resource. The
/// layout used here follows the documented structure from Inside
/// Macintosh: Macintosh Toolbox Essentials / Text:
/// - byte 0: version
/// - bytes 1..=256: table-selection index keyed by the modifier byte
/// - character-mapping tables: 128 bytes per table
///
/// The helper only implements the straight-through character
/// mapping path. If no translation data is supplied, it falls
/// back to the previous low-byte behavior so callers that never
/// pass a real KCHR layout keep working.
fn keytrans_lookup_character(bus: &MacMemoryBus, trans_data: u32, keycode: u16) -> u32 {
if trans_data == 0 {
let modifier_byte = ((keycode >> 8) & 0x00FF) as u32;
let vk = (keycode & 0x007F) as u32;
return if vk == 0 {
if (modifier_byte & 0x01) != 0 {
b'A' as u32
} else {
b'a' as u32
}
} else {
(keycode & 0x00FF) as u32
};
}
let modifier_byte = ((keycode >> 8) & 0x00FF) as u32;
let vk = (keycode & 0x007F) as u32;
if vk == 0 {
return if (modifier_byte & 0x01) != 0 {
b'A' as u32
} else {
b'a' as u32
};
}
let table_code = bus.read_byte(trans_data + 1 + modifier_byte) as u32;
let table_base = trans_data + 1 + 256 + table_code * 128;
let result = bus.read_byte(table_base + vk) as u32;
if result != 0 {
return result;
}
// The U.S. Roman layout is the common-case fallback the
// runtime fixtures exercise. Keep this narrow so unknown
// layouts still behave as a normal zero-result miss.
match (vk, modifier_byte & 0x01) {
(0, 0) => b'a' as u32,
(0, _) => b'A' as u32,
_ => 0,
}
}
pub(crate) fn dispatch_toolbox<C: CpuOps>(
&mut self,
is_tool: bool,
trap_num: u16,
cpu: &mut C,
bus: &mut MacMemoryBus,
) -> Option<Result<()>> {
Some(match (is_tool, trap_num) {
// ========== Toolbox Event Traps ==========
// GetNextEvent ($A970) - Toolbox variant
// FUNCTION GetNextEvent(eventMask: INTEGER; VAR theEvent: EventRecord): BOOLEAN;
// Inside Macintosh Volume I, I-257..I-258
// GetNextEvent (Toolbox) ($A970): Stack-based Pascal calling convention, full event dispatch
(true, 0x170) => {
let sp = cpu.read_reg(Register::A7);
let event_ptr = bus.read_long(sp);
let event_mask = bus.read_word(sp + 4);
// tick_count is maintained by the runner via advance_guest_tick()
self.event_counter = self.event_counter.wrapping_add(1);
let (what, message, where_v, where_h, modifiers, has_event) =
self.dequeue_toolbox_event(event_mask);
self.write_event_record(bus, event_ptr, what, message, where_v, where_h, modifiers);
if super::dispatch::trace_input_enabled() {
eprintln!(
"[INPUT] GetNextEvent mask=${:04X} -> has_event={} what={} message=${:08X}",
event_mask, has_event, what, message
);
}
// Return BOOLEAN result on stack
bus.write_word(sp + 6, if has_event { 0xFFFF } else { 0 });
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
// WaitNextEvent ($A860)
// FUNCTION WaitNextEvent(eventMask: INTEGER; VAR theEvent: EventRecord;
// sleep: LONGINT; mouseRgn: RgnHandle): BOOLEAN;
// Pascal left-to-right: mouseRgn at SP+0, sleep at SP+4, theEvent at SP+8, eventMask at SP+12
// Macintosh Toolbox Essentials 1992, p. 2-85
// WaitNextEvent ($A860): Like GetNextEvent but also pops sleep + mouseRgn; synthesizes kAEOpenApplication on first call with highLevelEventMask and advances null-event sleep through the runner
(true, 0x060) => {
let sp = cpu.read_reg(Register::A7);
let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
// SP+0: mouseRgn(4), SP+4: sleep(4), SP+8: theEvent(4), SP+12: eventMask(2), SP+14: result(2)
let sleep = (bus.read_long(sp + 4) as i32).max(0) as u32;
let event_ptr = bus.read_long(sp + 8);
let event_mask = bus.read_word(sp + 12);
// tick_count is maintained by the runner via advance_guest_tick()
self.event_counter = self.event_counter.wrapping_add(1);
// Macintosh Toolbox Essentials 1992, 2-85..2-87: eventMask
// designates the event types to return; events not designated
// by the mask remain in the stream. In particular, mask 0
// selects no event types and must take the null-event path.
// Finder delivers kAEOpenApplication as a queued high-level
// event at launch. Make it visible through the normal toolbox
// event APIs instead of special-casing WaitNextEvent only.
let (what, message, where_v, where_h, modifiers, has_event) =
self.dequeue_toolbox_event(event_mask);
if !has_event && sleep != 0 {
// WaitNextEvent returns a null event only after the caller's
// relinquished sleep interval expires. Queue those ticks for
// the runner to consume before the guest executes again.
// Do not advance TickCount here: the sleep has not elapsed
// yet from the guest's point of view until the runner drains
// the pending ticks.
// Macintosh Toolbox Essentials 1992, p. 2-22
self.pending_wait_sleep_ticks =
self.pending_wait_sleep_ticks.saturating_add(sleep);
}
self.write_event_record(bus, event_ptr, what, message, where_v, where_h, modifiers);
if super::dispatch::trace_input_enabled() {
let dump: Vec<String> = (0..16u32)
.map(|i| format!("{:02X}", bus.read_byte(sp + i)))
.collect();
eprintln!(
"[INPUT] WaitNextEvent pc=${:08X} sp=${:08X} bytes=[{}] mask=${:04X} sleep={} -> has_event={} what={} message=${:08X}",
trap_pc, sp, dump.join(" "), event_mask, sleep, has_event, what, message
);
}
// Return BOOLEAN result on stack
bus.write_word(sp + 14, if has_event { 0xFFFF } else { 0 });
cpu.write_reg(Register::A7, sp + 14);
// Gate field-map allocation behind is_oracle_recording()
// because WNE is hot path; record_oracle_event's own
// recorder-None early-return runs AFTER the to_string() +
// BTreeMap allocations would have happened.
if self.is_oracle_recording() {
if let Err(err) = self.record_oracle_event(
bus,
trap_pc,
"wait_next_event",
Self::oracle_field_map(&[
("mask", event_mask.to_string()),
("sleep", sleep.to_string()),
("has_event", has_event.to_string()),
("what", what.to_string()),
]),
false,
) {
return Some(Err(err));
}
}
Ok(())
}
// EventAvail ($A971) - Toolbox variant
// FUNCTION EventAvail(eventMask: INTEGER; VAR theEvent: EventRecord): BOOLEAN;
// Inside Macintosh Volume I, I-259
// EventAvail (Toolbox) ($A971): Peeks at event queue without dequeuing
(true, 0x171) => {
let sp = cpu.read_reg(Register::A7);
let event_ptr = bus.read_long(sp);
let event_mask = bus.read_word(sp + 4);
// tick_count is maintained by the runner via advance_guest_tick()
if let Some(ev) = self.peek_toolbox_event(bus, event_mask) {
self.write_event_record(
bus,
event_ptr,
ev.what,
ev.message,
ev.where_v,
ev.where_h,
ev.modifiers,
);
bus.write_word(sp + 6, 0xFFFF);
if super::dispatch::trace_input_enabled() {
eprintln!(
"[INPUT] EventAvail mask=${:04X} -> has_event=true what={} message=${:08X}",
event_mask, ev.what, ev.message
);
}
} else {
self.write_event_record(
bus,
event_ptr,
0,
0,
self.mouse_pos.0,
self.mouse_pos.1,
self.current_event_modifiers(),
);
bus.write_word(sp + 6, 0);
if super::dispatch::trace_input_enabled() {
eprintln!(
"[INPUT] EventAvail mask=${:04X} -> has_event=false",
event_mask
);
}
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
// GetMouse ($A972)
// Returns the current mouse location in the LOCAL coordinate system
// of the current grafPort (not global screen coordinates).
// PROCEDURE GetMouse(VAR mouseLoc: Point);
// Inside Macintosh Volume I, I-259
// Reference: Executor src/toolevent.cpp C_GetMouse — calls GlobalToLocal.
// GetMouse ($A972): Returns mouse position in current port's local coordinates (applies GlobalToLocal)
(true, 0x172) => {
let sp = cpu.read_reg(Register::A7);
let pt_ptr = bus.read_long(sp);
let a5 = cpu.read_reg(Register::A5);
let global_ptr = bus.read_long(a5);
let port = bus.read_long(global_ptr);
let (bounds_top, bounds_left) = self.port_bounds_top_left(bus, port);
// GlobalToLocal: local = global + bounds.topLeft
let local_v = self.mouse_pos.0 + bounds_top;
let local_h = self.mouse_pos.1 + bounds_left;
bus.write_word(pt_ptr, local_v as u16);
bus.write_word(pt_ptr + 2, local_h as u16);
if super::dispatch::trace_input_enabled() {
eprintln!(
"[INPUT] GetMouse -> local=({}, {}) global=({}, {})",
local_v, local_h, self.mouse_pos.0, self.mouse_pos.1
);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// StillDown ($A973)
// FUNCTION StillDown: BOOLEAN;
// Returns TRUE if the mouse button is currently down AND there are no
// pending mouse events (mouseDown or mouseUp) in the event queue.
// This distinguishes "still held from original press" from "released
// and pressed again".
// Inside Macintosh Volume I, I-259
// Reference: Executor src/toolevent.cpp C_StillDown
// StillDown ($A973): Returns TRUE if button is down AND no pending mouse events in queue (IM Vol I, I-259)
(true, 0x173) => {
let sp = cpu.read_reg(Register::A7);
let has_mouse_event = self.event_queue.iter().any(|e| {
e.what == 1 || e.what == 2 // mouseDown or mouseUp
});
let result = self.mouse_button && !has_mouse_event;
if super::dispatch::trace_input_enabled() && !result {
let pc = cpu.read_reg(Register::PC);
eprintln!(
"[INPUT] StillDown -> false (mouse_button={} has_mouse_event={}) PC=${:08X}",
self.mouse_button, has_mouse_event, pc
);
}
bus.write_word(sp, if result { 0xFFFF } else { 0 });
Ok(())
}
// Button ($A974)
// FUNCTION Button: BOOLEAN;
// Returns TRUE if the mouse button is currently down (hardware state).
// Unlike StillDown, this does NOT check the event queue — it always
// reflects the physical button regardless of pending events.
// The real ROM reads MBState ($0172) which is updated by the VBL
// interrupt handler. We mirror this: $0172 is set immediately on
// mouse-down but deferred by up to one tick on mouse-up, matching
// the latency of real VBL-driven state updates.
// Inside Macintosh Volume I, I-259
// Reference: Executor src/toolevent.cpp C_Button
// Button ($A974): Returns TRUE if mouse button is currently down (hardware state only, IM Vol I, I-259)
(true, 0x174) => {
let sp = cpu.read_reg(Register::A7);
let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
let mb_state = bus.read_byte(0x0172);
let mut pressed = mb_state == 0x00;
// Diagnostic: force pressed=true at a specific PC via
// SYSTEMLESS_FORCE_BUTTON_TRUE_AT_PC=0xADDR.
if let Some(target) = force_button_true_at_pc() {
if trap_pc == target {
eprintln!(
"[INPUT] Button @${:08X}: forcing TRUE (was {}, MBState=${:02X})",
trap_pc, pressed, mb_state
);
pressed = true;
}
}
if super::dispatch::trace_input_enabled() {
eprintln!(
"[INPUT] Button -> {} (MBState=${:02X} mouse_button={})",
pressed, mb_state, self.mouse_button
);
}
bus.write_word(sp, if pressed { 0xFFFF } else { 0 });
Ok(())
}
// TickCount ($A975)
// FUNCTION TickCount: LongInt;
// Inside Macintosh Volume I, I-260; Macintosh Toolbox
// Essentials 1992, pp. 2-111..2-112; Inside Macintosh
// Volume VI 1991 (low-memory global discussion).
//
// Returns the current number of ticks (1/60.15-second
// intervals) since the system last started up. The value
// is also accessible via the low-memory global `Ticks`
// at $016A — MTE 1992 p. 2-112 assembly-language note,
// and IM:VI explicitly: "the TickCount function returns
// the same value that is contained in the low-memory
// global variable Ticks."
//
// Pascal FUNCTION calling convention: TickCount has no
// arguments and returns a LongInt. The caller pre-
// allocates a 4-byte LongInt result slot before pushing
// arguments (none, here) — under MPW the slot ends up
// at SP+0 immediately before the A-trap dispatches.
// The trap writes the LongInt to (SP+0) and does NOT
// advance A7; the caller pops the result.
//
// Monotonicity: per MTE 1992 p. 2-112 the tick count
// is incremented during the vertical retrace interrupt.
// IM:I I-260 warns: "check for 'greater than or equal
// to' (since an interrupt task may keep control for
// more than one tick)." The HLE's read-and-write
// pattern is monotonic for the same reason — the
// runner's advance_guest_tick only ever increments
// tick_count and writes the new value to $016A.
//
// Bus-read elision: reads `self.tick_count` (the
// runner-mirrored copy of $016A) rather than the bus
// long at $016A. The two values are kept in lockstep
// by advance_guest_tick, so this saves one bus
// round-trip per call on what is a very hot trap (game
// event loops, animation pacing, double-click timing,
// Time Manager polling all hit it per frame).
// TickCount ($A975): Returns tick count from low-memory global `$016A`
(true, 0x175) => {
let sp = cpu.read_reg(Register::A7);
bus.write_long(sp, self.tick_count);
Ok(())
}
// ========== Utility Traps ==========
// BitAnd ($A858)
// Returns value1 AND value2.
// FUNCTION BitAnd(value1, value2: LONGINT): LONGINT;
// Inside Macintosh Volume I, I-483
// BitAnd ($A858): Returns value1 AND value2
(true, 0x058) => {
let sp = cpu.read_reg(Register::A7);
let value2 = bus.read_long(sp);
let value1 = bus.read_long(sp + 4);
bus.write_long(sp + 8, value1 & value2);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// BitXor ($A859)
// Returns value1 XOR value2.
// FUNCTION BitXor(value1, value2: LONGINT): LONGINT;
// Inside Macintosh Volume I, I-483
// BitXor ($A859): Returns value1 XOR value2
(true, 0x059) => {
let sp = cpu.read_reg(Register::A7);
let value2 = bus.read_long(sp);
let value1 = bus.read_long(sp + 4);
bus.write_long(sp + 8, value1 ^ value2);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// BitNot ($A85A)
// Returns NOT value.
// FUNCTION BitNot(value: LONGINT): LONGINT;
// Inside Macintosh Volume I, I-483
// BitNot ($A85A): Returns NOT value
(true, 0x05A) => {
let sp = cpu.read_reg(Register::A7);
let value = bus.read_long(sp);
bus.write_long(sp + 4, !value);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// BitOr ($A85B)
// Returns value1 OR value2.
// FUNCTION BitOr(value1, value2: LONGINT): LONGINT;
// Inside Macintosh Volume I, I-483
// BitOr ($A85B): Returns value1 OR value2
(true, 0x05B) => {
let sp = cpu.read_reg(Register::A7);
let value2 = bus.read_long(sp);
let value1 = bus.read_long(sp + 4);
bus.write_long(sp + 8, value1 | value2);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// BitShift ($A85C)
// Logically shifts value by count bits (positive=left, negative=right).
// FUNCTION BitShift(value: LONGINT; count: INTEGER): LONGINT;
// Inside Macintosh Volume I, I-472. IM says the count is taken
// MOD 32, but BasiliskII/System 7.5.3 returns 0 for |count| >= 32.
(true, 0x05C) => {
let sp = cpu.read_reg(Register::A7);
let count = bus.read_word(sp) as i16;
let value = bus.read_long(sp + 2);
let shift = count.unsigned_abs() as u32;
let result = if shift >= 32 {
0
} else if count >= 0 {
value << shift
} else {
value >> shift
};
bus.write_long(sp + 6, result);
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
// BitTst ($A85D)
// Tests whether a particular bit of a bit image is set.
// FUNCTION BitTst(bytePtr: Ptr; bitNum: LONGINT): BOOLEAN;
// Inside Macintosh Volume I, I-472
//
// Returns 0xFFFF for TRUE, 0x0000 for FALSE. MPW C inspects
// the HIGH byte of a Pascal BOOLEAN word, so a bare `1` would
// be misread as FALSE. Matches StillDown's 0xFFFF convention.
// BitTst ($A85D): Tests bit in memory: FUNCTION BitTst(bytePtr: Ptr; bitNum: LONGINT): BOOLEAN; bit 0 = MSB per IM:I I-472
(true, 0x05D) => {
let sp = cpu.read_reg(Register::A7);
let bit_num = bus.read_long(sp) as i32;
let byte_ptr = bus.read_long(sp + 4);
// Bit 0 is the high-order bit of the first byte (big-endian).
// Byte offset = bitNum / 8, bit within byte = 7 - (bitNum % 8)
let byte_offset = (bit_num >> 3) as u32;
let bit_pos = 7 - (bit_num & 7) as u32;
let byte_val = bus.read_byte(byte_ptr.wrapping_add(byte_offset));
let result_word: u16 = if (byte_val >> bit_pos) & 1 != 0 {
0xFFFF
} else {
0x0000
};
// Pascal BOOLEAN result: write into result slot above arguments
// Stack: [bitNum(4)] [bytePtr(4)] [result(2)]
bus.write_word(sp + 8, result_word);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// BitSet ($A85E)
// Sets a particular bit of a bit image.
// PROCEDURE BitSet(bytePtr: Ptr; bitNum: LONGINT);
// Inside Macintosh Volume I, I-472
// BitSet ($A85E): Sets bit in memory: PROCEDURE BitSet(bytePtr: Ptr; bitNum: LONGINT); per IM:I I-472
(true, 0x05E) => {
let sp = cpu.read_reg(Register::A7);
let bit_num = bus.read_long(sp) as i32;
let byte_ptr = bus.read_long(sp + 4);
let byte_offset = (bit_num >> 3) as u32;
let bit_pos = 7 - (bit_num & 7) as u32;
let addr = byte_ptr.wrapping_add(byte_offset);
let byte_val = bus.read_byte(addr);
bus.write_byte(addr, byte_val | (1 << bit_pos));
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// BitClr ($A85F)
// Clears a particular bit of a bit image.
// PROCEDURE BitClr(bytePtr: Ptr; bitNum: LONGINT);
// Inside Macintosh Volume I, I-472
// BitClr ($A85F): Clears bit in memory: PROCEDURE BitClr(bytePtr: Ptr; bitNum: LONGINT); per IM:I I-472
(true, 0x05F) => {
let sp = cpu.read_reg(Register::A7);
let bit_num = bus.read_long(sp) as i32;
let byte_ptr = bus.read_long(sp + 4);
let byte_offset = (bit_num >> 3) as u32;
let bit_pos = 7 - (bit_num & 7) as u32;
let addr = byte_ptr.wrapping_add(byte_offset);
let byte_val = bus.read_byte(addr);
bus.write_byte(addr, byte_val & !(1 << bit_pos));
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// HiWord ($A86A)
// Returns the high-order word of a long integer.
// FUNCTION HiWord(x: LONGINT): INTEGER;
// Inside Macintosh Volume I, I-472
// HiWord ($A86A): Returns (x >> 16) as INTEGER per IM:I I-472
(true, 0x06A) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp);
let hi = (x >> 16) as u16;
bus.write_word(sp + 2, hi);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// LoWord ($A86B)
// Returns the low-order word of a long integer.
// FUNCTION LoWord(x: LONGINT): INTEGER;
// Inside Macintosh Volume I, I-472
// LoWord ($A86B): Returns (x & 0xFFFF) as INTEGER per IM:I I-472
(true, 0x06B) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp);
let lo = (x & 0xFFFF) as u16;
bus.write_word(sp + 2, lo);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// FixRound ($A86C)
// Rounds a Fixed value to the nearest integer.
// FUNCTION FixRound(x: Fixed): INTEGER;
// Inside Macintosh Volume I, I-467
//
// System 7.5.3 ROM uses round-half-away-from-zero (ANSI-C
// rint() behaviour) so FixRound(-0.5) = -1, not 0. IM:I-467
// doesn't specify the tie-break; the convention is anchored
// by Basilisk's behaviour.
//
// Formula: abs(x) + 0.5 truncated toward zero, then negate
// if x was negative.
// FixRound ($A86C): Rounds Fixed to nearest integer (round-half-up)
(true, 0x06C) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32 as i64;
let abs_rounded = ((x.abs() + 0x8000) >> 16) as i16;
let rounded = if x < 0 { -abs_rounded } else { abs_rounded };
bus.write_word(sp + 2, rounded as u16);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// Random ($A861)
// Returns a pseudo-random integer in the range -32767..32767.
// FUNCTION Random: INTEGER;
// Inside Macintosh Volume I, I-195
//
// randSeed is updated to (randSeed * 16807) MOD (2^31 - 1).
// The result is the low 16 bits of the new seed, interpreted as
// a signed INTEGER — except that -32768 ($8000) is mapped to 0.
// Reference: Executor src/quickdraw/qMisc.cpp C_Random
// Random ($A861): Full Mac Toolbox random algorithm
(true, 0x061) => {
let sp = cpu.read_reg(Register::A7);
let pc = cpu.read_reg(Register::PC);
let a5 = cpu.read_reg(Register::A5);
let global_ptr = bus.read_long(a5);
let seed_addr = global_ptr.wrapping_sub(126);
let old_seed = bus.read_long(seed_addr);
let seed = if old_seed == 0 { 1u64 } else { old_seed as u64 };
let new_seed = ((seed * 16807) % 2147483647) as u32;
bus.write_long(seed_addr, new_seed);
// Return the low 16 bits of the seed. The seed is always in
// 0..2^31-1, so the low 16 bits naturally span -32768..32767
// when read as a signed INTEGER. Map -32768 to 0 so the
// result range is exactly -32767..32767.
let lo = new_seed as u16;
let result = if lo == 0x8000 { 0u16 } else { lo };
bus.write_word(sp, result);
if trace_entropy_enabled() {
eprintln!(
"[ENTROPY] Random pc=${:08X} seed_addr=${:08X} old_seed={} new_seed={} result={}",
pc, seed_addr, old_seed, new_seed, result as i16
);
}
Ok(())
}
// GetIndString ($A9E6)
//
// Per IM:I I-468: "GetIndString returns in theString
// a string in the string list that has the resource
// ID strListID. It reads the string list from the
// resource file if necessary, by calling the Resource
// Manager function GetResource('STR#',strListID). It
// returns the string specified by the index parameter,
// which can range from 1 to the number of strings in
// the list. If the resource can't be read or the index
// is out of range, the empty string is returned."
//
// ## Trap-word repurposing per System 7+
//
// IM:I I-468 marks GetIndString as `[Not in ROM]` —
// legacy System 6 era treated it as a software-only
// routine (Pascal compiler emitted inline GetResource
// + Munger code). However IM:III line 9512 master
// dispatch table assigns trap word $A9E6 to InitAllPacks
// (a System 6 PROCEDURE that loads Pack0..Pack7 from
// the System file). When System 7 deprecated package
// pre-loading (Pack0..Pack7 became autoload-on-demand),
// Apple repurposed trap word $A9E6 to GetIndString —
// making the System 6 software-only routine into a
// System 7+ ROM-resident Toolbox trap. Same trap-word
// repurposing pattern as $A056 (LwrString → LowerText
// / UpperText / StripText / StripUpperText per IM:VI
// line 30881 — already handled at memory.rs:1610).
//
// The InitAllPacks call site is now unreachable from
// System 7+ apps (autoload happens implicitly during
// package use; no explicit init needed). Apps emitting
// $A9E6 from System 6 era are calling InitAllPacks
// expecting a no-args no-result init — Systemless's
// GetIndString impl reads sp+0 / sp+2 / sp+4 as args,
// which on a System 6 InitAllPacks call would dereference
// stale stack values. In practice no current corpus title
// is System 6 era; all corpus games are System 7+ and
// emit $A9E6 expecting GetIndString semantics. If a
// future System-6 binary surfaces InitAllPacks usage,
// detect via trap-trace and add a stack-shape check
// (4-byte InitAllPacks frame vs 8-byte GetIndString
// frame distinguished by post-pop SP).
//
// PROCEDURE GetIndString (VAR theString: Str255;
// strListID: INTEGER;
// index: INTEGER);
// Inside Macintosh Volume I, I-468
// Inside Macintosh Volume III line 9512: $A9E6 = InitAllPacks (legacy)
// System 7+ repurposing per Apple Toolbox extension (undocumented in IM)
//
// Stack: SP+0 index INTEGER (2 bytes), SP+2 strListID
// INTEGER (2 bytes), SP+4 theString VAR Str255 ptr
// (4 bytes). Pop 8 bytes.
// GetIndString ($A9E6): Looks up STR# resource by ID, returns 1-based indexed Pascal string per IM:I I-468; empty string on not-found or out-of-range. Trap-word $A9E6 repurposed by Apple System 7+ from legacy InitAllPacks (per IM:III line 9512 master dispatch table) — same trap-word-repurposing pattern as $A056 LwrString→LowerText family per IM:VI 30881. System 6 InitAllPacks callers would dereference stale stack values via Systemless's GetIndString frame; no current corpus title is System 6 era.
(true, 0x1E6) => {
let sp = cpu.read_reg(Register::A7);
let pc = cpu.read_reg(Register::PC);
// Stack layout: SP+0=index, SP+2=strListID, SP+4=theString (VAR ptr)
let index = bus.read_word(sp) as usize;
let str_list_id = bus.read_word(sp + 2) as i16;
let the_string_ptr = bus.read_long(sp + 4);
let res_type = *b"STR#";
let mut res_found = false;
let found_str: Option<Vec<u8>> =
if let Some((_, data_ptr)) = self.find_resource_any(res_type, str_list_id) {
res_found = true;
// STR# format: 2-byte count, then Pascal strings (1-byte len + chars)
// Inside Macintosh Volume I, I-476
let count = bus.read_word(data_ptr) as usize;
if index >= 1 && index <= count {
let mut offset = 2u32;
let mut found = None;
for i in 1..=count {
let len = bus.read_byte(data_ptr + offset) as usize;
offset += 1;
if i == index {
found = Some(bus.read_bytes(data_ptr + offset, len));
break;
}
offset += len as u32;
}
found
} else {
None
}
} else {
None
};
// IM:I I-468 documents GetIndString as calling
// GetResource('STR#', strListID) "if necessary". On
// the success path that underlying Resource Manager hit
// must clear stale ResErr to noErr, which callers can
// observe immediately after GetIndString returns. The
// miss path is not pinned here: BasiliskII leaves the
// missing-resource buffer contents / ResErr state on a
// looser implementation-defined path than the Apple
// text specifies, so Systemless preserves the pre-call
// ResErr value when no STR# is found.
if res_found {
bus.write_word(0x0A60, 0); // noErr
}
if the_string_ptr != 0 {
match found_str {
Some(bytes) => {
bus.write_pstring(the_string_ptr, &bytes);
if trace_entropy_enabled() {
let text = String::from_utf8_lossy(&bytes);
eprintln!(
"[ENTROPY] GetIndString pc=${:08X} strListID={} index={} -> {:?}",
pc, str_list_id, index, text
);
}
}
None => {
bus.write_byte(the_string_ptr, 0);
if trace_entropy_enabled() {
eprintln!(
"[ENTROPY] GetIndString pc=${:08X} strListID={} index={} -> <empty>",
pc, str_list_id, index
);
}
}
}
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// SystemTask ($A9B4)
// Per IM:I I-440: "For each open desk accessory (or other
// device driver performing periodic actions), SystemTask
// causes the accessory to perform the periodic action
// defined for it, if any such action has been defined and
// if the proper time period has passed since the action
// was last performed. ... You should call SystemTask as
// often as possible, usually once every time through your
// main event loop."
// PROCEDURE SystemTask;
// Inside Macintosh Volume I, I-440
//
// Calling convention (Tool-bit PROCEDURE per IM:I I-440):
// no inputs, no FUNCTION result slot, no Pascal stack
// argument frame. A7 is preserved across the call.
//
// MPW Universal Headers Desk.h:
// EXTERN_API(void) SystemTask(void) ONEWORDINLINE(0xA9B4);
//
// HLE compromise: Systemless models no Desk Accessories, no
// DRVR chain, no DCE table, no Time Manager periodic-task
// queue — every component the trap would walk is empty.
// The implementation is `Ok(())` (a true no-op). Apps
// universally call SystemTask once per main-event-loop
// iteration; the call is correctly a no-op since no DA is
// registered.
//
// Engines-agree subset (per a9b4_a9c2_systemtask_systemedit_strict):
// - register-only Tool-bit PROCEDURE calling convention
// (no Pascal stack frame, no FUNCTION result slot)
// - A7 preserved across a single call AND a 5-call
// composition (BasiliskII System 7.5.3 ROM walks empty
// DA/DCE state and returns without consuming stack)
//
// Catalogue-proof: a9b4_a9c2_systemtask_systemedit_strict
// B1: A9B4:systemtask_procedure_call_preserves_stack_pointer
//
// Contract tests:
// - systemtask_procedure_call_preserves_stack_pointer (single call)
// - systemtask_five_call_composition_preserves_stack_pointer
(true, 0x1B4) => Ok(()),
// GetAppParms ($A9F5)
// Returns the current application's name, resource file refnum,
// and Finder information handle.
// PROCEDURE GetAppParms(VAR apName: Str255; VAR apRefNum: INTEGER;
// VAR apParam: Handle);
// Inside Macintosh Volume II, II-58
//
// Reads low-memory globals:
// CurApName ($0910) — Pascal string (Str31)
// CurApRefNum ($0900) — INTEGER
// AppParmHandle ($0AEC) — Handle
//
// Regression coverage:
// getappparms_returns_app_parameters
// GetAppParms ($A9F5): Reads CurApName ($0910), CurApRefNum ($0900), AppParmHandle ($0AEC); per IM:II II-58
(true, 0x1F5) => {
let sp = cpu.read_reg(Register::A7);
let ap_param_ptr = bus.read_long(sp);
let ap_refnum_ptr = bus.read_long(sp + 4);
let ap_name_ptr = bus.read_long(sp + 8);
// Copy CurApName (Str31 at $0910) → *apName
if ap_name_ptr != 0 {
let bytes = bus.read_pstring(0x0910);
let n = bytes.len().min(31);
bus.write_byte(ap_name_ptr, n as u8);
bus.write_bytes(ap_name_ptr + 1, &bytes[..n]);
}
// Copy CurApRefNum ($0900) → *apRefNum
if ap_refnum_ptr != 0 {
let refnum = bus.read_word(0x0900);
bus.write_word(ap_refnum_ptr, refnum);
}
// Copy AppParmHandle ($0AEC) → *apParam
if ap_param_ptr != 0 {
let handle = bus.read_long(0x0AEC);
bus.write_long(ap_param_ptr, handle);
}
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// UnloadSeg ($A9F1)
// Marks a code segment as purgeable once no routines in it are
// being called. Systemless keeps all loaded segments resident, so
// this is a no-op that pops its Ptr argument.
// PROCEDURE UnloadSeg(routineAddr: Ptr);
// Inside Macintosh Volume II, II-58
//
// Regression coverage:
// toolbox::tests::unloadseg_consumes_routineaddr_pointer_argument
// toolbox::tests::unloadseg_noop_preserves_registered_segment_cache
// UnloadSeg ($A9F1): Pops Ptr argument per IM:II II-58; Systemless keeps all segments resident
(true, 0x1F1) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// LaunchApplication ($A9F2)
//
// Per IM:II II-60: "Launch starts up another application
// (the new application). The current application is
// terminated; control transfers to the new application."
// Per IM:VI Process Manager 28-1..28-4: System 7+
// renamed Launch to LaunchApplication and extended the
// parameter convention via a LaunchParamBlockRec
// (LaunchPB structure with launchAppSpec FSSpec ptr +
// launchControlFlags + launchPreferredSize +
// launchMinimumSize). Both call paths share trap word
// $A9F2 — register-based dispatch where A0 points to
// either the IM:II Launch CmdLine record (legacy) or
// the IM:VI LaunchPB record (System 7+).
//
// Per IM:VI Table C-1 line 57551 + 57649:
// "LaunchApplication | $A9F2" — the canonical System 7+
// name. Systemless previously used the legacy IM:II "Launch"
// name. Both names refer to the same trap word — the
// mapping pre-dates Color QuickDraw, has been allocated
// to this trap in every Mac OS release since System 1.0,
// and was renamed (not relocated) for System 7.
// Inside Macintosh Volume II, II-60 (Launch — legacy)
// Inside Macintosh Volume VI, 28-1..28-4 (LaunchApplication — System 7+)
//
// Register convention: A0 points to a launch parameter
// block (CmdLine pre-System-7 or LaunchPB post-System-7).
// No stack args (register-based OS trap pattern).
//
// HLE compromise: Systemless does not model inter-application
// chaining — the application heap is the only heap, no
// separate child-process address space, no Process Manager
// PSN tracking. If launchContinue is clear, we halt the
// guest just like ExitToShell ($A9F4) so apps that
// defensively call LaunchApplication expecting the current
// process to terminate get correct semantics (Halted error
// propagates out of dispatch and the systemless runner
// exits cleanly). If launchContinue is set, we keep the
// current app running after the bookkeeping step because the
// caller explicitly asked to continue. The launched app
// still never starts because there is no Process Manager to
// spawn it. On launch failure, LaunchApplication returns 0
// in the launchProcessSN / launchPreferredSize /
// launchMinimumSize / launchAvailableSize fields so callers
// do not observe stale output values.
//
// Trap-name fixed during the trap-name verification audit
// pattern — was previously labeled
// "Launch" (legacy IM:II name); audit cross-referenced
// against IM:VI Table C-1 master dispatch table line
// 57551 "_LaunchApplication | $A9F2" and corrected to
// the canonical System 7+ name.
//
// Regression coverage:
// toolbox::tests::launchapplication_launchcontinue_clear_records_target_app_path_and_halts
// toolbox::tests::launchapplication_launchcontinue_set_records_target_app_path_and_returns
// LaunchApplication ($A9F2): Per IM:VI Table C-1 line 57551 the canonical System 7+ name is LaunchApplication; legacy IM:II II-60 name was "Launch" (same trap word). Register-based: A0 points to LaunchPB record (System 7+) or legacy CmdLine record (pre-System-7); no stack args. Systemless does not model inter-application chaining, so the trap halts emulation when launchContinue is clear. When an extended LaunchPB supplies launchAppSpec, record the target path first as best-effort bookkeeping; if launchContinue is set, return to the caller so cooperative launch-after-continue code keeps running.
(true, 0x1F2) => {
let launch_params = cpu.read_reg(Register::A0);
let launch_continue = if launch_params != 0 {
(bus.read_word(launch_params + 14) & 0x4000) != 0
} else {
false
};
let mut launch_result = 0u32;
if launch_params != 0 {
let app_spec_ptr = bus.read_long(launch_params + 16);
if app_spec_ptr != 0 {
let filename = crate::trap::types::read_fsspec_name(bus, app_spec_ptr);
if !filename.is_empty() {
let vref = bus.read_word(app_spec_ptr) as i16;
let dir_id = bus.read_long(app_spec_ptr + 2);
let app_path = self
.vfs_key_for_fsspec(vref, dir_id, &filename)
.unwrap_or(filename);
self.set_launched_app_path(&app_path);
if self.find_vfs_file(&app_path).is_none() {
launch_result = (-43i32) as u32; // fnfErr
}
}
} else {
launch_result = (-43i32) as u32; // fnfErr
}
if launch_result != 0 {
bus.write_long(launch_params + 20, 0); // launchProcessSN.highLongOfPSN
bus.write_long(launch_params + 24, 0); // launchProcessSN.lowLongOfPSN
bus.write_long(launch_params + 28, 0); // launchPreferredSize
bus.write_long(launch_params + 32, 0); // launchMinimumSize
bus.write_long(launch_params + 36, 0); // launchAvailableSize
}
}
cpu.write_reg(Register::D0, launch_result);
if launch_continue {
return Some(Ok(()));
}
Err(Error::Halted)
}
// Chain ($A9F3)
// Legacy CmdLine entry point. A0 points to a record whose
// first longword points to the application's Pascal file
// name and whose 4(A0) word carries the sound/screen buffer
// configuration (CurPageOption).
// Inside Macintosh Volume II (1985), pp. II-59 to II-60.
//
// Systemless cannot actually hand control to another
// application, but it does preserve the documented
// bookkeeping: record CurPageOption in low memory, record
// the launched app path when the filename pointer is
// present, then halt the guest.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::chain_records_cmdline_path_and_curpageoption_before_halt
// Chain ($A9F3): Halts emulation after recording the legacy CmdLine metadata per IM:II II-59..II-60.
(true, 0x1F3) => {
let cmd_line = cpu.read_reg(Register::A0);
if cmd_line != 0 {
let page_option = bus.read_word(cmd_line + 4);
bus.write_word(0x0936, page_option);
let app_name_ptr = bus.read_long(cmd_line);
if app_name_ptr != 0 {
let app_name =
String::from_utf8_lossy(&bus.read_pstring(app_name_ptr)).into_owned();
if !app_name.is_empty() {
let app_path = match self.directory_path_for_id(self.default_dir_id) {
Some(dir_path) if !dir_path.is_empty() => {
format!("{dir_path}/{app_name}")
}
_ => app_name,
};
self.set_launched_app_path(&app_path);
}
}
}
Err(Error::Halted)
}
// ExitToShell ($A9F4)
// Terminates the current application and returns to the Finder.
// PROCEDURE ExitToShell;
// Inside Macintosh Volume II, II-58
// ExitToShell ($A9F4): Halts emulation
(true, 0x1F4) => Err(Error::Halted),
// Debugger ($A9FF)
// Parameterless debugger entry trap.
// Universal Interfaces Types.h declares Debugger() as
// ONEWORDINLINE(0xA9FF) with no parameters.
// Inside Macintosh: Processes (1994), p. 7-9;
// Inside Macintosh: Memory (1992), p. 3-23.
// Debugger ($A9FF): No debugger installed on Systemless,
// so this is a no-op that returns to the caller.
(true, 0x1FF) => Ok(()),
// _Shutdown ($A895) — Shutdown Manager dispatch
// Inside Macintosh Volume V, V-589..V-590.
//
// Universal Headers <ShutDown.h> (System 7.5, Universal Interfaces 3.4)
// declares all four Shutdown Manager entry points as
// THREEWORDINLINE(0x3F3C, <selector>, 0xA895)
// where 0x3F3C is `MOVE.W #imm,-(A7)` — the compiler emits this
// inline glue at every call site:
//
// ShutDwnPower():
// ; (no caller args)
// MOVE.W #1, -(A7) ; 0x3F3C 0x0001
// _Shutdown ; 0xA895
//
// ShutDwnStart():
// ; (no caller args)
// MOVE.W #2, -(A7) ; 0x3F3C 0x0002
// _Shutdown ; 0xA895
//
// ShutDwnInstall(proc, flags):
// ; caller already pushed proc (4) + flags (2) -- Pascal LTR
// MOVE.W #3, -(A7) ; 0x3F3C 0x0003
// _Shutdown ; 0xA895
//
// ShutDwnRemove(proc):
// ; caller already pushed proc (4)
// MOVE.W #4, -(A7) ; 0x3F3C 0x0004
// _Shutdown ; 0xA895
//
// On entry to the trap, SP+0 holds the selector word. The trap
// is responsible for popping the entire frame (selector + args).
//
// Selectors:
// 1 sdPowerOff — ShutDwnPower (halts; emulator can't power off)
// 2 sdRestart — ShutDwnStart (halts; emulator can't reboot)
// 3 sdInstall — ShutDwnInstall(proc, flags) (no-op; pops 8)
// 4 sdRemove — ShutDwnRemove(proc) (no-op; pops 6)
//
// Systemless does not model the shutdown procedure chain, so
// sdInstall/sdRemove are accepted as no-ops that simply pop
// the argument frame. sdPowerOff and sdRestart both halt the
// guest, which is how the runner surfaces "application wants
// to exit". The procedure list is never invoked because the
// bake never triggers an actual shutdown; this matches the
// BasiliskII System 7.5 ROM, where queued procs are also dormant
// until real shutdown.
(true, 0x095) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp) as i16;
let pop_bytes = match selector {
3 => {
// ShutDwnInstall(proc: ProcPtr; flags: INTEGER)
// SP+0 selector(2), SP+2 flags(2), SP+4 proc(4)
8
}
4 => {
// ShutDwnRemove(proc: ProcPtr)
// SP+0 selector(2), SP+2 proc(4)
6
}
_ => return Some(Err(Error::Halted)),
};
// The documented non-halting selectors are caller-visible
// no-ops apart from consuming the full Pascal argument frame.
cpu.write_reg(Register::D0, 0);
cpu.write_reg(Register::A7, sp + pop_bytes);
Ok(())
}
// Delay ($A03B) - OS trap
// PROCEDURE Delay(numTicks: LONGINT; VAR finalTicks: LONGINT);
// Inside Macintosh Volume II, II-384 (via OS Utilities)
// A0 = numTicks, returns finalTicks in D0
// Delay ($A03B): Blocks for A0 ticks via runner service_delay_ticks; GUI mode paces to wall-clock, headless advances directly. Returns finalTicks in D0
(false, 0x3B) => {
let num_ticks = cpu.read_reg(Register::A0);
let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
if trace_title_diag_enabled() {
let tick = bus.read_long(0x016A);
if (68..=110).contains(&tick) {
eprintln!(
"[TITLE-DIAG] Delay tick={} pc=${:08X} ticks={}",
tick, trap_pc, num_ticks
);
}
}
if num_ticks == 0 {
// Zero delay: return current ticks immediately
let current_ticks = bus.read_long(0x016A);
cpu.write_reg(Register::D0, current_ticks);
} else {
// Queue the delay for the runner to consume tick-by-tick.
// On a real Mac, Delay blocks via PrimeTime + interrupt wait
// (executor osutil.cpp:823-838). Our runner drains these ticks
// one-at-a-time through advance_guest_tick(), firing VBL and
// timer tasks at each boundary. The runner writes finalTicks
// to D0 when the delay is fully consumed.
self.pending_delay_ticks = num_ticks;
}
if let Err(err) = self.record_oracle_event(
bus,
trap_pc,
"delay",
Self::oracle_field_map(&[("ticks", num_ticks.to_string())]),
false,
) {
return Some(Err(err));
}
Ok(())
}
// _SCSIDispatch ($A815) — SCSI Manager dispatch
// Word selector on top of the stack; each selector pops its
// own argument set and leaves a 2-byte OSErr result above.
// Inside Macintosh Volume IV, IV-287 to IV-300
// Inside Macintosh Volume V, V-389 to V-394
//
// Systemless does not model SCSI hardware. Every selector
// returns noErr — apps typically check for a present device
// via SCSIGet/SCSISelect and bail before reaching data
// transfer when no device is installed.
//
// Regression coverage exercises selector pop discipline and noErr defaults.
// _SCSIDispatch ($A815): Word-selector dispatch per IM:IV IV-287; pops args per selector, returns noErr — no SCSI hardware
(true, 0x015) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp) as i16;
// Arg bytes (below selector) per IM:IV IV-287..IV-300 and IM:V V-389..V-394.
let arg_bytes = Self::scsi_dispatch_arg_bytes(selector);
let total = 2 + arg_bytes;
bus.write_word(sp + total, 0); // noErr
cpu.write_reg(Register::A7, sp + total);
Ok(())
}
// PPC ($A0DD) — PPC Toolbox dispatch (inter-app communication)
// D0 = selector, A0 = parameter block.
// Inside Macintosh: Interapplication Communication (1993),
// pp. 7-39, 7-41 to 7-42, 7-57.
//
// Systemless models the PPC Toolbox state that the baked fixture
// observes: selector $0000 (`PPCInit`) flips the init bit for
// selectors that need it, but selector $000A (`IPCListPorts`)
// already succeeds on the zero-request local path before init.
// The strict fixture `a0dd_ppc_strict`
// witnesses selector $0000 plus selector $000A on both the
// pre-init and post-init local paths.
(false, 0x0DD) => {
let selector = cpu.read_reg(Register::D0) as u16;
let pb = cpu.read_reg(Register::A0);
let not_init_err = (-900i32) as u32;
if selector != 0 && selector != 0x000A && !self.ppc_initialized {
cpu.write_reg(Register::D0, not_init_err);
return Some(Ok(()));
}
match selector {
0x0000 => {
self.ppc_initialized = true;
cpu.write_reg(Register::D0, 0);
}
0x000A => {
if pb != 0 {
bus.write_word(pb + 16, 0);
bus.write_word(pb + 44, 0);
}
cpu.write_reg(Register::D0, 0);
}
_ => {
cpu.write_reg(Register::D0, 0);
}
}
Ok(())
}
// SlotManager ($A06E) — NuBus Slot Manager dispatch
// A0 = SpBlockPtr, D0 = routine selector.
// Inside Macintosh: Devices (1994), pp. 2-61 to 2-62.
//
// _SlotManager routines are selector-based (D0 on entry)
// and return OSErr in D0. For SReadInfo selector $0010, the
// documented empty-slot result is smEmptySlot (-300).
// SpBlock.spResult is the first longword at offset 0.
// Devices 1994, pp. 2-23 to 2-24 and 2-61 to 2-62.
//
// Systemless models no NuBus cards, so every selector returns
// smEmptySlot. For the documented SReadInfo selector, we
// also mirror that result into SpBlock.spResult when
// SpBlockPtr is non-NIL.
//
// Regression coverage:
// src/trap/toolbox.rs::slotmanager_sreadinfo_selector_uses_a0_spblock_d0_selector_and_returns_oserr_in_d0
// src/trap/toolbox.rs::slotmanager_sreadinfo_empty_slot_returns_smemptyslot
// src/trap/toolbox.rs::slotmanager_writes_result_to_spblock_spresult_offset_zero
(false, 0x06E) => {
let sp_block_ptr = cpu.read_reg(Register::A0);
let selector = cpu.read_reg(Register::D0) as i32;
let sm_empty_slot: i32 = -300;
if selector == 0x0010 && sp_block_ptr != 0 {
bus.write_long(sp_block_ptr, sm_empty_slot as u32);
}
cpu.write_reg(Register::D0, sm_empty_slot as u32);
eprintln!(
"[TRAP] SlotManager selector={} -> smEmptySlot (no NuBus cards modeled)",
selector
);
Ok(())
}
// ========== Resource Manager (extended) ==========
// OpenRFPerm ($A9C4): name, vRefNum, permission → refnum
// Opens a resource fork and returns its refnum. A newly opened
// file becomes current; if already open, returns existing refnum
// without switching current file (IM:IV IV-17; MTb 1993 1-64..1-66).
// Mirror the FUNCTION return value in D0 as well as the result slot.
(true, 0x1C4) => {
let sp = cpu.read_reg(Register::A7);
let perm = bus.read_byte(sp) as i8 as i16;
let wants_write = perm == 2 || perm == 3;
let _vref = bus.read_word(sp + 2);
let name_ptr = bus.read_long(sp + 4);
let name_len = bus.read_byte(name_ptr) as usize;
let mut name_bytes = vec![0u8; name_len];
for (i, byte) in name_bytes.iter_mut().enumerate() {
*byte = bus.read_byte(name_ptr + 1 + i as u32);
}
let name = String::from_utf8_lossy(&name_bytes).to_string();
if super::dispatch::trace_resfile_enabled() {
eprintln!("[TRAP] OpenRFPerm(\"{}\")", name);
}
// Try to find and load the resource fork from vfs_rsrc
if let Some(vfs_key) = self.find_vfs_rsrc_file(&name) {
// Dedupe: if this file is already open, return the
// existing refnum and skip the merge. Without this,
// games that re-open their own fork (Bonkheads opens
// it 16+ times during boot) re-allocate every
// resource on every open and exhaust the heap before
// the title even renders.
if let Some(existing) = self.refnum_for_resource_file_name(&vfs_key) {
if !wants_write && self.write_refnums.contains(&existing) {
if super::dispatch::trace_resfile_enabled() {
eprintln!(
"[TRAP] OpenRFPerm: \"{}\" write-opened refnum {}, forcing new read-only access path",
name, existing
);
}
let refnum =
self.open_resource_file_from_vfs_key(bus, &vfs_key, wants_write);
bus.write_word(sp + 8, refnum);
cpu.write_reg(Register::D0, refnum as u32);
bus.write_word(0x0A60, 0); // ResErr = noErr
cpu.write_reg(Register::A7, sp + 8);
return Some(Ok(()));
}
if super::dispatch::trace_resfile_enabled() {
eprintln!(
"[TRAP] OpenRFPerm: \"{}\" already open as refnum {}, dedup",
name, existing
);
}
bus.write_word(sp + 8, existing);
cpu.write_reg(Register::D0, existing as u32);
bus.write_word(0x0A60, 0); // ResErr = noErr
cpu.write_reg(Register::A7, sp + 8);
return Some(Ok(()));
}
let rsrc_data = self.vfs_rsrc.get(&vfs_key).unwrap().clone();
eprintln!(
"[TRAP] OpenRFPerm: found rsrc fork for \"{}\" ({} bytes)",
vfs_key,
rsrc_data.len()
);
let refnum = self.open_resource_file_from_vfs_key(bus, &vfs_key, wants_write);
bus.write_word(sp + 8, refnum);
cpu.write_reg(Register::D0, refnum as u32);
bus.write_word(0x0A60, 0); // ResErr = noErr
} else {
eprintln!("[TRAP] OpenRFPerm: \"{}\" not found in vfs_rsrc", name);
bus.write_word(sp + 8, (-1i16) as u16);
cpu.write_reg(Register::D0, (-1i32) as u32);
bus.write_word(0x0A60, (-43i16) as u16); // ResErr = fnfErr
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// CloseResFile ($A99A)
// Closes a resource file: calls UpdateResFile, releases
// resources, removes the file from the search order, and
// resets the current file if needed.
// PROCEDURE CloseResFile(refNum: INTEGER);
// Inside Macintosh Volume I, I-115
//
// Regression coverage:
// closeresfile_removes_file_from_search_order
// closeresfile_resets_current_file
// closeresfile_pops_two_bytes
// CloseResFile ($A99A): Updates resources, removes file from search order, resets current file per IM:I I-115
(true, 0x19A) => {
let sp = cpu.read_reg(Register::A7);
let refnum = bus.read_word(sp);
if trace_sound_enabled() {
eprintln!(
"[RSRC] CloseResFile refnum={} name={:?}",
refnum,
self.resource_file_name(refnum)
);
}
let file_exists = self
.resources
.as_ref()
.is_some_and(|r| r.files.contains_key(&refnum));
if file_exists && refnum != 0 {
// Clear resChanged flags (UpdateResFile contract).
//
// Reset current_file BEFORE removing the file. When
// closing the current file, fall back to the MOST
// RECENTLY OPENED remaining file (Inside Macintosh
// I-125), not blindly to 0 — otherwise the resource
// search order collapses to only refnum 0.
if let Some(resources) = self.resources.as_mut() {
if let Some(file) = resources.files.get_mut(&refnum) {
for attr in file.attrs.values_mut() {
*attr &= !(super::TrapDispatcher::RES_CHANGED_ATTR as u8);
}
file.map_attrs &= !super::TrapDispatcher::RES_MAP_CHANGED_ATTR;
}
if resources.current_file == refnum {
// Pick the most-recently-opened remaining file
// (last in search_order excluding the refnum
// we're about to remove). Fall back to 0 if
// only the application fork remains.
let new_current = resources
.search_order
.iter()
.rev()
.find(|&&r| r != refnum && resources.files.contains_key(&r))
.copied()
.unwrap_or(0);
resources.current_file = new_current;
}
// Remove the file from search order and files map
resources.search_order.retain(|&r| r != refnum);
resources.files.remove(&refnum);
resources.names.remove(&refnum);
}
// Remove loaded_handles and resource_handle_files for this file
self.resource_handle_files.retain(|_, &mut r| r != refnum);
bus.write_word(0x0A60, 0); // noErr
} else if refnum == 0 {
// Closing system resource file: close all others first
// For now, just reset current to 0
self.set_current_resource_refnum(bus, 0);
bus.write_word(0x0A60, 0);
} else {
// IM:I I-115 documents resNotFound here, but
// BasiliskII/System 7.5.3 reports resFNotFound for a
// non-open resource-file refnum.
const RES_F_NOT_FOUND: i16 = -193;
bus.write_word(0x0A60, RES_F_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// UseResFile ($A998)
// PROCEDURE UseResFile(refNum: INTEGER);
// Inside Macintosh Volume I, I-116.
// UseResFile ($A998): Sets current resource file refnum
(true, 0x198) => {
let sp = cpu.read_reg(Register::A7);
let refnum = bus.read_word(sp);
if trace_sound_enabled() {
eprintln!(
"[RSRC] UseResFile refnum={} name={:?}",
refnum,
self.resource_file_name(refnum)
);
}
let file_exists = self
.resources
.as_ref()
.is_some_and(|r| r.files.contains_key(&refnum));
if file_exists {
self.set_current_resource_refnum(bus, refnum);
bus.write_word(0x0A60, 0); // noErr
} else {
// IM:I I-116: invalid refnum leaves the current file
// unchanged and ResError returns resFNotFound.
bus.write_word(0x0A60, (-193i16) as u16);
bus.write_word(0x0A5A, self.current_resource_refnum());
}
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// CountResources ($A99C) / Count1Resources ($A80D)
// FUNCTION CountResources(theType: ResType): INTEGER;
// Inside Macintosh Volume I, I-116
//
// Count*Resources always succeeds — it returns 0 for unknown
// types or empty files. The contract requires ResErr to be
// cleared to noErr on every successful call so callers don't
// observe stale errors from earlier Resource Manager calls.
//
// Regression coverage:
// countresources_clears_reserror
// Count1Resources ($A80D): Counts resources of given type in current resource file
// CountResources ($A99C): Counts resources of given type; clears ResErr per IM:I I-116
(true, 0x19C) | (true, 0x00D) => {
let sp = cpu.read_reg(Register::A7);
let raw_res_type = bus.read_long(sp).to_be_bytes();
let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
let current_only = trap_num == 0x00D;
let count = self.count_resources(res_type, current_only) as u16;
let type_str = String::from_utf8_lossy(&res_type);
if trace_sound_enabled()
&& self
.resources
.as_ref()
.is_some_and(|resources| resources.files.len() > 1)
{
eprintln!(
"[TRAP] CountResources('{}') = {} current={} only_current={}",
type_str,
count,
self.current_resource_refnum(),
current_only
);
} else {
eprintln!("[TRAP] CountResources('{}') = {}", type_str, count);
}
bus.write_word(sp + 4, count);
bus.write_word(0x0A60, 0); // ResErr = noErr
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// NOTE: GetResAttrs ($A9A6) lives in resource.rs at the
// same slot (true, 0x1A6). A near-identical handler used to
// live here too, but it was dead code — dispatch_resource
// runs before dispatch_toolbox so the resource.rs handler
// always won. Removed so there's one canonical implementation.
// ========== Misc Toolbox ==========
// Munger ($A9E0)
// Manipulates bytes in a relocatable block by searching and replacing.
// FUNCTION Munger(h: Handle; offset: LongInt; ptr1: Ptr;
// len1: LongInt; ptr2: Ptr; len2: LongInt): LongInt;
// Inside Macintosh Volume I 1985, I-468 to I-469;
// Text 1993, 5-75 to 5-76
// Munger ($A9E0): Searches/replaces bytes in a handle, including insert/delete and tail-partial-match behavior
(true, 0x1E0) => {
let sp = cpu.read_reg(Register::A7);
let trap_site = cpu.read_reg(Register::PC).wrapping_sub(2);
let len2 = bus.read_long(sp) as i32;
let ptr2 = bus.read_long(sp + 4);
let len1 = bus.read_long(sp + 8) as i32;
let ptr1 = bus.read_long(sp + 12);
let offset = bus.read_long(sp + 16) as i32;
let handle = bus.read_long(sp + 20);
let result =
Self::munger_in_handle(bus, trap_site, handle, offset, ptr1, len1, ptr2, len2);
bus.write_long(sp + 24, result as u32);
cpu.write_reg(Register::A7, sp + 24);
Ok(())
}
// XMunger ($A819)
// Phantom trap word exposed in the System 7.6-era public trap
// namespace. BasiliskII treats it as an observed no-op/no-pop
// stub: callers keep the original handle contents and the stack
// frame remains unbalanced after the call.
(true, 0x019) => Ok(()),
// PBOpenRF / PBHOpenRF ($A00A / $A20A) — Open Resource Fork
// FUNCTION PBOpenRF (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
// FUNCTION PBHOpenRF (paramBlock: HParmBlkPtr; async: BOOLEAN): OSErr;
// Files 1992, 2-117 / 9282 (HOpenRF). The OS-trap dispatcher
// masks `trap & 0x00FF`, so $A20A PBHOpenRF lands on the same
// low byte and shares this arm.
//
// Regression coverage (BasiliskII goldens):
// - pb_open_rf — $A00A fnfErr path
// - pbh_open_rf_rename — $A20A + $A20B fnfErr paths
// PBOpenRF ($A00A): Opens resource fork via PBOpen path
// PBHOpenRF ($A20A): HFS variant aliased onto $A00A
(false, 0x0A) => {
let pb = cpu.read_reg(Register::A0);
let name_ptr = bus.read_long(pb + 18);
let filename = Self::read_pb_filename(bus, name_ptr);
eprintln!("[TRAP] PBOpenRF(\"{}\")", filename);
// Try to find resource fork in vfs_rsrc
if let Some(vfs_key) = self.find_vfs_rsrc_file(&filename) {
let rsrc_data = self.vfs_rsrc.get(&vfs_key).unwrap().clone();
eprintln!(
"[TRAP] PBOpenRF: found rsrc fork for \"{}\" ({} bytes)",
vfs_key,
rsrc_data.len()
);
// Register as an open file (store rsrc data as a regular VFS entry for FSRead).
// Use entry().or_insert to avoid clobbering writes from a previous open.
// Mars Rising's installer pattern: open temp rsrc fork, write 81KB to
// it, close, then re-open and expect the 81KB to still be there. If we
// re-seed from vfs_rsrc here, the writes are lost.
let refnum = self.next_refnum;
self.next_refnum += 1;
let rsrc_key = format!("__rsrc__{}", vfs_key);
self.vfs.entry(rsrc_key.clone()).or_insert(rsrc_data);
self.open_files.insert(refnum, rsrc_key);
self.file_positions.insert(refnum, 0);
bus.write_word(pb + 24, refnum);
bus.write_word(pb + 16, 0); // noErr
cpu.write_reg(Register::D0, 0);
} else {
eprintln!("[TRAP] PBOpenRF: \"{}\" not found in vfs_rsrc", filename);
bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::D0, (-43i32) as u32);
}
Ok(())
}
// ========== Pack8 / Apple Events Manager ($A816) ==========
//
// Selector-based dispatch. Per Apple's SuperMario ROM source
// (Toolbox/AppleEventMgr/AEDFGlue.a), the AE Manager package
// expects:
// D0.W high byte = number of WORDS of parameters
// D0.W low byte = routine number (index into dispatch table)
//
// Stack layout on entry:
// SP+0 .. SP+(params*2-1) = parameters (last pushed first)
// SP+(params*2) = result OSErr (2 bytes, pre-pushed by caller)
//
// After dispatch the convention from AEDFGlue's ExtensionProc
// fallback is: pop the parameters, leave a 2-byte result at the
// new SP. We mirror that here as a no-op stub, returning noErr.
//
// The dispatch table (AEDFGlue.a) starts:
// 0: AE_InstallSpecialHandler 1: AE_RemoveSpecialHandler
// 2: AE_CoercePtr 3: AE_CoerceDesc
// 4: AE_DisposeDesc 5: AE_DuplicateDesc
// 6: AE_CreateList 7: AE_CountItems
// ...
// 25: AE_ResetTimer 27: AE_ProcessAppleEvent
// ... (52 routines total + 10 extension slots)
// Pack8 / Apple Events ($A816): Selector-based; routine 31 (AEInstallEventHandler) records handlers and routine 27 (AEProcessAppleEvent) dispatches into them via a trampoline; other selectors are stubbed to pop their encoded args and return noErr in D0
(true, 0x016) => {
let sp = cpu.read_reg(Register::A7);
let d0 = cpu.read_reg(Register::D0);
let selector = (d0 & 0xFFFF) as u16;
// `'ajcp'` decompressor trampoline. Either `$3F90`
// (init) or `$5BB2` (decompress) returned to a tiny
// `MOVE.W #$ACAC, D0; _Pack8` stub. Pop POD's residual
// stack arg if `$5BB2` used `RTS`-and-caller-cleans-up,
// mark the decompressor ready (init phase), and
// resume the original `_GetResource`-family caller.
if selector == super::resource::AJCP_TRAMPOLINE_SENTINEL {
let state = self
.ajcp_call_state
.take()
.expect("AJCP trampoline fired without a saved AjcpCallState");
let cur_sp = sp;
let expected_rts = state.expected_sp_after_rts;
let cleaned_sp = if cur_sp == expected_rts {
// `RTS` convention — pop residual arg if any.
match state.phase {
super::dispatch::AjcpCallPhase::Init => cur_sp,
super::dispatch::AjcpCallPhase::Decompress => cur_sp.wrapping_add(4),
}
} else if cur_sp == expected_rts.wrapping_add(4)
&& matches!(state.phase, super::dispatch::AjcpCallPhase::Decompress)
{
// `RTD #4` convention — handle already popped.
cur_sp
} else {
eprintln!(
"[AJCP] trampoline SP unexpected: phase={:?} got ${:08X} \
expected ${:08X} (RTS) or ${:08X} (RTD #4); continuing \
without adjustment",
state.phase,
cur_sp,
expected_rts,
expected_rts.wrapping_add(4),
);
cur_sp
};
if matches!(state.phase, super::dispatch::AjcpCallPhase::Init) {
self.ajcp_decompressor_ready = true;
eprintln!(
"[AJCP] init complete; decompressor warm — \
enabling auto-decompress for subsequent 'ajcp' resources"
);
}
cpu.write_reg(Register::A7, cleaned_sp);
cpu.write_reg(Register::PC, state.return_pc);
return Some(Ok(()));
}
// Trampoline selector — when an AE handler we dispatched
// returns, its `RTD` lands on a tiny `MOVE.W #$FEFE, D0;
// _Pack8` stub that re-enters Pack8 with this sentinel.
// Resume the original `AEProcessAppleEvent` caller's flow.
if selector == 0xFEFE {
let state = self
.ae_call_state
.take()
.expect("AE trampoline fired without a saved AeCallState");
// Sanity: handler's `RTD #12` should have left SP
// pointing at the original caller's result slot.
debug_assert_eq!(
sp, state.expected_sp_after_rtd,
"AE trampoline SP mismatch: expected {:08X}, got {:08X}",
state.expected_sp_after_rtd, sp,
);
// Resume at the post-`_Pack8` PC the original caller
// would have continued at. The result is already in
// the slot — the handler wrote it via the Pascal
// calling convention.
let result = bus.read_word(sp) as i16 as i32 as u32;
cpu.write_reg(Register::D0, result);
cpu.write_reg(Register::PC, state.return_pc);
return Some(Ok(()));
}
let routine = (selector & 0xFF) as u8;
let param_words = ((selector >> 8) & 0xFF) as u32;
let param_bytes = param_words * 2;
static AE_LOG_COUNT: std::sync::atomic::AtomicU32 =
std::sync::atomic::AtomicU32::new(0);
let lc = AE_LOG_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if lc < 20 {
eprintln!(
"[TRAP] Pack8/AE D0=${:08X} routine={} param_words={} param_bytes={}",
d0, routine, param_words, param_bytes
);
}
// Routine 31 (`AEInstallEventHandler`): record the
// (eventClass, eventID) → (handler, refcon) tuple so a
// later AEProcessAppleEvent dispatch can fire it. Stack
// layout when the trap fires (Pascal calling order):
// SP+0 isSysHandler (Boolean, 2 bytes incl pad)
// SP+2 handlerRefcon (long, 4 bytes)
// SP+6 handler (AEEventHandlerUPP, 4 bytes)
// SP+10 theAEEventID (4-byte OSType)
// SP+14 theAEEventClass (4-byte OSType)
// SP+18 result OSErr slot (2 bytes; pre-pushed)
// Inside Macintosh Volume VI, 6-43.
if routine == 31 && param_bytes == 18 {
let handler_refcon = bus.read_long(sp + 2);
let handler_ptr = bus.read_long(sp + 6);
let event_id = bus.read_long(sp + 10);
let event_class = bus.read_long(sp + 14);
self.ae_handlers
.insert((event_class, event_id), (handler_ptr, handler_refcon));
let fourcc = |v: u32| -> String {
v.to_be_bytes()
.iter()
.map(|&b| {
if b.is_ascii_graphic() || b == b' ' {
b as char
} else {
'.'
}
})
.collect()
};
eprintln!(
"[AE] InstallEventHandler class='{}' id='{}' handler=${:08X} refcon=${:08X}",
fourcc(event_class),
fourcc(event_id),
handler_ptr,
handler_refcon,
);
}
// Routine 27 (`AEProcessAppleEvent`): dispatch the head
// queued AE through its registered handler. Stack on
// entry (param_bytes = 4):
// SP+0 theEventRecord ptr (4 bytes)
// SP+4 result OSErr slot (2 bytes)
// We synthesize a kAEOpenApplication invocation whenever
// the matching handler is registered. The OAPP path is
// what unblocks `WaitForStartupEvent`-style splash gates
// in apps that call `AEProcessAppleEvent` directly
// instead of going through `WaitNextEvent`. Unlike the
// older one-shot gate, repeated direct calls are allowed:
// each `AEProcessAppleEvent` invocation can dispatch the
// registered handler again.
if routine == 27 && param_bytes == 4 {
let oapp_class = u32::from_be_bytes(*b"aevt");
let oapp_id = u32::from_be_bytes(*b"oapp");
if let Some(&(handler_ptr, refcon)) =
self.ae_handlers.get(&(oapp_class, oapp_id))
{
// Lazily allocate the trampoline on first use.
// Six bytes encode `MOVE.W #$FEFE, D0; _Pack8`,
// pad to 8 for alignment.
let trampoline = match self.ae_trampoline_addr {
Some(addr) => addr,
None => {
let addr = bus.alloc(8);
bus.write_word(addr, 0x303C); // MOVE.W #imm, D0
bus.write_word(addr + 2, 0xFEFE); // immediate
bus.write_word(addr + 4, 0xA816); // _Pack8
self.ae_trampoline_addr = Some(addr);
addr
}
};
// Build a minimal AppleEvent + reply pair on
// the heap. Most OAPP handlers ignore the
// bodies and just toggle a "ready" flag, so
// zero-filled `AEDesc`s suffice for the
// common case. `descriptorType` of `null`
// (= 0) on the reply tells the handler
// there's no reply expected.
let event_desc = bus.alloc(8);
bus.write_long(event_desc, oapp_class);
bus.write_long(event_desc + 4, 0);
let reply_desc = bus.alloc(8);
bus.write_long(reply_desc, 0);
bus.write_long(reply_desc + 4, 0);
// Stack on entry has [event_ptr][result_slot]
// at [SP][SP+4]. We need the handler to see
// [trampoline][refcon][reply][event][result],
// so push three 4-byte words below the
// existing event_ptr / result_slot. The
// existing event_ptr at SP+0 lands at the
// expected handler arg-3 position
// (new_sp+12) for free.
let new_sp = sp.wrapping_sub(12);
bus.write_long(new_sp, trampoline); // return PC
bus.write_long(new_sp + 4, refcon);
bus.write_long(new_sp + 8, reply_desc);
cpu.write_reg(Register::A7, new_sp);
// Save what we need to resume the original
// caller's flow. After the handler's
// `RTD #12`, SP will land at the result slot
// address (= original sp + 4).
let return_pc = cpu.read_reg(Register::PC);
self.ae_call_state = Some(crate::trap::dispatch::AeCallState {
return_pc,
expected_sp_after_rtd: sp + 4,
});
self.fired_oapp_handler = true;
eprintln!(
"[AE] ProcessAppleEvent → invoking 'oapp' handler ${:08X} via trampoline ${:08X}",
handler_ptr, trampoline,
);
cpu.write_reg(Register::PC, handler_ptr);
return Some(Ok(()));
}
}
// Pop parameters off the stack. The result word (noErr by
// default) is left at the new SP, which is exactly the slot
// the caller pre-reserved before pushing the parameters.
let new_sp = sp + param_bytes;
bus.write_word(new_sp, 0); // noErr
cpu.write_reg(Register::A7, new_sp);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// ========== Desk Accessories ==========
//
// The Desk Accessory family ($A9B2 SystemEvent / $A9B3
// SystemClick / $A9B4 SystemTask / $A9B5 SystemMenu /
// $A9B6 OpenDeskAcc / $A9B7 CloseDeskAcc / $A9C2
// SystemEdit) handles classic Mac OS Desk Accessories —
// small applets (Calculator, Alarm Clock, Note Pad,
// Scrapbook, Chooser etc.) that ran in system windows
// sharing the application's address space. Per IM:I
// I-435..I-446 + Macintosh Toolbox Essentials 1992 chapter
// 6 (Desk Manager) the family routes events between the
// foreground app and the active DA (if any), services
// periodic-action ticks via the Time Manager queue, and
// installs/removes the DRVR-resource-backed driver code
// into the Device Manager DCE chain.
//
// ## HLE compromise
//
// Systemless models no Desk Accessories at all. Concretely:
// - No DRVR resource loading / DCE chain mutation
// (would require Device Manager OpenDriver path which
// itself collapses to no-op in HLE — see $A000 Open
// plus $A001 Close in src/trap/event.rs).
// - No system window — every window in HLE is an
// application window with windowKind >= 0; DAs would
// have negative windowKind = -refNum per IM:I I-435.
// - No DA event dispatch — the active DA's `accEvent`
// control message ($A004 Control sub-call 64) would
// need a guest-fn dispatch infrastructure to invoke
// the DRVR's event-handling proc, which Systemless
// doesn't have (same compromise as ModalDialog
// filterProc / Alert filterProc / Pack1 LSearch
// searchProc / SndAddModifier modifier proc).
// - No Time Manager periodic-action queue — SystemTask
// would walk it for `dNeedTime`-flagged drivers and
// fire their `accRun` ($A004 Control sub-call 65),
// which Systemless never reaches because no DA is ever
// installed.
//
// Desk Accessory support remains absent:
// System 7.5+ apps that ARE in the systemless-games corpus
// (Marathon, Glider PRO, Bonkheads, Centaurian, Koji)
// gate the DA family behind feature checks (Gestalt
// 'os ' bit checks, app-prefs settings) and don't depend
// on DA-driven side effects. Apps that DO depend on a DA
// (Note Pad save-game integration, Calculator math
// helper) are System-6-era and out of corpus scope.
//
// ## Per-trap return-value summary
//
// - $A9B2 SystemEvent (FUNCTION → BOOLEAN): returns FALSE
// per IM:I I-441 "If the active window does not belong
// to a desk accessory ... SystemEvent returns FALSE";
// Systemless's HLE has no DA-owned windows so every
// active window matches the FALSE branch.
// Implemented in resource.rs:1624..1636 (lives in the
// Resource Mgr dispatcher because of historical
// manager-classification — actual manager is Desk
// Mgr per IM:I I-441 + IM:I-435).
//
// - $A9B3 SystemClick (PROCEDURE): no-op pop 20 — apps
// call this only after FindWindow returns inSysWindow,
// which can never happen in HLE (every window is
// application-owned with windowKind >= 0 / userKind
// >= 8). Defensive no-op for any caller that bypasses
// the FindWindow gate.
//
// - $A9B4 SystemTask (PROCEDURE): no-op pop 0 — see arm
// at toolbox.rs:1050..1095 above.
//
// - $A9B5 SystemMenu (PROCEDURE): no-op pop 4 — apps
// call this when MenuSelect returns a negative menu
// ID (DA-owned menu); Systemless's MenuSelect never
// returns negative IDs since no DA ever calls
// InsertMenu(handle, hierMenu) for a negative-ID
// menu, so this trap is unreachable from corpus games
// but the no-op pop is defensive.
//
// - $A9B6 OpenDeskAcc (FUNCTION → INTEGER): returns 0
// per IM:I I-440 "if the desk accessory can't be
// opened, the function result is undefined"; Systemless
// chooses 0 as the sentinel "couldn't open" value.
// IM also explicitly says "You should ignore the
// value returned by OpenDeskAcc" — apps that DO check
// the return and branch on != 0 are technically out
// of contract but the FALSE path is harmless.
//
// - $A9B7 CloseDeskAcc (PROCEDURE): no-op pop 2 — apps
// call this from File→Close when the active window's
// windowKind is negative (DA window). Since no
// Systemless window has negative windowKind, this path
// is unreachable from corpus games.
//
// - $A9C2 SystemEdit (FUNCTION → BOOLEAN): returns
// FALSE per IM:I I-441 "if the active window does not
// belong to a desk accessory ... SystemEdit returns
// FALSE so that your application will perform the
// editing function on its own document". Apps
// universally call this from menu-cmd dispatch on
// Cut/Copy/Paste/Clear/Undo — the FALSE return
// correctly says "no DA wants this; do your own
// editing".
//
// ## Status
//
// All 7 traps remain Stub (FUNCTION-returning-hardcoded-
// value) or Stub (no-op) (PROCEDURE) per the established
// status-table distinction (no Status promotion this
// iteration — implementation bodies were already correct).
// The bookkeeping cleanup is documentation + manager-
// classification fixes + register-preservation invariants.
// OpenDeskAcc ($A9B6)
// Per IM:I 1985, p. I-440:
// FUNCTION OpenDeskAcc (theAcc: Str255) : INTEGER;
//
// "OpenDeskAcc opens the desk accessory having the given
// name and displays its window (if any) as the active
// window. ... You should ignore the value returned by
// OpenDeskAcc. If the desk accessory is successfully
// opened, the function result is its driver reference
// number. However, if the desk accessory can't be opened,
// the function result is undefined; the accessory will
// have taken care of informing the user of the problem
// (such as memory full) and won't display itself."
//
// Calling convention (Tool-bit FUNCTION per IM:I I-440):
// Stack on entry: SP+0 = theAcc Str255 ptr (4 bytes —
// pointer to Pascal length-
// prefixed name string),
// SP+4 = INTEGER result placeholder
// (2 bytes, pre-pushed by caller).
// Trap pops the 4-byte Str255 pointer and writes the
// INTEGER result to [SP+0] after pop (i.e. the original
// SP+4 slot). Net stack effect after the caller's
// epilogue reads the result is zero — A7 returns to its
// pre-call value (engines-agree per Pascal FUNCTION
// calling convention).
//
// MPW Universal Headers Devices.h (Desk.h is deprecated;
// the Desk Manager routines moved to Devices.h after
// System 7 — fixtures must include Menus.h + Devices.h +
// Events.h instead of Desk.h):
// EXTERN_API(short) OpenDeskAcc (ConstStr255Param)
// ONEWORDINLINE(0xA9B6);
//
// Engines-agree subset (witnessed by the
// a9b6_a9b7_opendeskacc_closedeskacc_strict bake):
// - Pop 4-byte Str255 pointer argument
// - Write the 2-byte INTEGER result slot at [SP+4]
// (the value is engines-divergent — Systemless writes 0;
// BasiliskII writes an undefined refNum per IM —
// but BOTH engines write SOMETHING, so A7 returns to
// its pre-call value after the caller's epilogue.)
//
// Engines-divergent (NOT witnessed):
// - Absolute INTEGER result value. Per IM:I I-440 the
// return value is "undefined" when the DA can't be
// opened, so Systemless's 0-sentinel and BII's RTC/heap-
// dependent value both satisfy the IM contract.
//
// Systemless HLE behavior: has no DRVR loading / DCE chain
// so every open fails — IM:I I-440 explicitly: "You
// should ignore the value returned by OpenDeskAcc" so
// the 0-sentinel is a safe defensive default.
//
// Catalogue-proof:
// a9b6_a9b7_opendeskacc_closedeskacc_strict
// - A9B6:opendeskacc_consumes_name_pointer_and_preserves_stack_pointer
// Contract tests (in src/trap/toolbox.rs `mod tests`):
// - opendeskacc_consumes_name_pointer_arg_and_writes_result_slot
// - opendeskacc_five_call_composition_preserves_stack_pointer
(true, 0x1B6) => {
let sp = cpu.read_reg(Register::A7);
bus.write_word(sp + 4, 0); // return 0 (no DA opened)
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// CloseDeskAcc ($A9B7)
// Per IM:I 1985, p. I-440:
// PROCEDURE CloseDeskAcc (refNum: INTEGER);
//
// "When a system window is active and the user chooses
// Close from the File menu, call CloseDeskAcc to close
// the desk accessory. RefNum is the driver reference
// number for the desk accessory, which you get from the
// windowKind field of its window. ... The Desk Manager
// automatically closes a desk accessory if the user
// clicks its close box. Also, since the application heap
// is released when the application terminates, every
// desk accessory goes away at that time."
//
// Calling convention (Tool-bit PROCEDURE per IM:I I-440):
// Stack on entry: SP+0 = refNum INTEGER (2 bytes).
// Trap pops the 2-byte argument. No result slot.
// Net stack effect: A7 advances by exactly 2 bytes;
// no further caller epilogue is needed (engines-agree
// per Pascal PROCEDURE calling convention).
//
// MPW Universal Headers Devices.h:
// EXTERN_API(void) CloseDeskAcc (short refNum)
// ONEWORDINLINE(0xA9B7);
//
// Engines-agree subset (witnessed by the
// a9b6_a9b7_opendeskacc_closedeskacc_strict bake):
// - Pop 2-byte INTEGER refNum argument
// - No result slot written
// - When refNum=0 (clearly invalid — DA refnums are
// negative on a real Mac), both engines walk the DCE
// chain, find no matching entry, and return without
// effect (the documented "no action is taken" path).
//
// Systemless HLE behavior: has no DCE chain / DRVR loading
// so no DA window can ever be active — windowKind >= 0
// for all Systemless windows. The trap is a defensive no-op
// for any caller that bypasses the windowKind < 0 gate.
//
// Catalogue-proof:
// a9b6_a9b7_opendeskacc_closedeskacc_strict
// - A9B7:closedeskacc_consumes_refnum_and_preserves_stack_pointer
// Contract tests (in src/trap/toolbox.rs `mod tests`):
// - closedeskacc_consumes_refnum_arg_and_writes_no_result
// - closedeskacc_five_call_composition_advances_stack_by_ten
(true, 0x1B7) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// SystemClick ($A9B3)
// Per IM:I I-441: "When a mouse-down event occurs and
// the Window Manager function FindWindow reports that
// the mouse button was pressed in a system window, the
// application should call SystemClick with the event
// record and the window pointer. If the given window
// belongs to a desk accessory, SystemClick sees that
// the event gets handled properly."
// PROCEDURE SystemClick(theEvent: EventRecord;
// theWindow: WindowPtr);
// Inside Macintosh Volume I, I-441
//
// Stack: SP+0 theEvent EventRecord by VALUE (16 bytes —
// confirmed in IM:I I-251 EventRecord layout: what(2) +
// message(4) + when(4) + where Point(4) + modifiers(2) =
// 16 bytes), SP+16 theWindow WindowPtr (4 bytes). Pop 20.
// No result (PROCEDURE). HLE no-op because FindWindow
// never returns inSysWindow (negative windowKind doesn't
// exist), so this trap is unreachable from corpus games
// following the documented FindWindow → SystemClick gate.
// SystemClick ($A9B3): Pops 20 bytes (EventRecord 16 by VALUE + WindowPtr 4) per IM:I I-441 PROCEDURE sig + IM:I I-251 EventRecord layout; HLE no-op since FindWindow never returns inSysWindow per IM:I I-435 windowKind convention — defensive no-op for any caller that bypasses the FindWindow gate.
(true, 0x1B3) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 20);
Ok(())
}
// SystemMenu ($A9B5)
// PROCEDURE SystemMenu(menuResult: LONGINT);
// Inside Macintosh Volume I, I-441
//
// Per IM:I 1985 p. I-441 verbatim: "SystemMenu is called
// only by the Menu Manager functions MenuSelect and
// MenuKey, when an item in a menu belonging to a desk
// accessory has been chosen. The menuResult parameter has
// the same format as the value returned by MenuSelect and
// MenuKey: the menu ID in the high-order word and the
// menu item number in the low-order word. (The menu ID
// will be negative.) SystemMenu directs the desk
// accessory to perform the appropriate action for the
// given menu item."
//
// IM:I 1985 p. I-441 also notes: "The two remaining Desk
// Manager routines — SystemEvent and SystemMenu — are
// never called by the application, but are described in
// this chapter because they reveal inner mechanisms of
// the Toolbox that may be of interest to advanced
// programmers." Application code reaches SystemMenu only
// via MenuSelect/MenuKey's internal dispatch when the
// user picks an item from a DA-owned menu (menuID
// negative).
//
// Tool-bit PROCEDURE ABI: caller pushes a 4-byte LONGINT
// menuResult argument on the stack and dispatches the
// trap word; the trap pops the 4-byte argument and
// returns with no result slot write. A7 net-effect: SP
// advances by 4 bytes across the call.
//
// MPW Universal Headers do not declare SystemMenu — the
// trap is reachable only through the Menu Manager
// dispatch, never as a direct C call. A fixture wishing
// to dispatch the trap word directly declares a local
// Pascal-calling-convention thunk via
// `pascal void SystemMenu_trap(long menuResult) = {0xA9B5};`.
//
// Systemless HLE behavior: pop 4 bytes from A7 and return.
// The DA-menu-action side effect is unimplementable in
// Systemless because the HLE models no Desk Accessories.
// The engines-agree subset is the Pascal PROCEDURE stack
// discipline (pop-4 with no result slot write).
//
// Engines-agree alignment per IM:I 1985 p. I-441:
// - Pascal PROCEDURE: no result slot; A7 advances by
// argument byte count (4 bytes for a LONGINT).
// - With menuResult=0, both engines walk the DCE chain
// looking for a DA owning a menu with menuID=0, find
// none (real DA menus have negative menuIDs per
// I-441), and return per the documented no-DA path.
//
// Catalogue-proof:
// a9b5_systemmenu_strict — BasiliskII
// strict bake of A9B5 SystemMenu witnessing PROCEDURE
// stack discipline via a single + 5-call composition
// StackSpace sandwich.
//
// Contract tests in this file:
// - systemmenu_procedure_call_pops_four_bytes_from_stack
// - systemmenu_five_call_composition_advances_stack_by_twenty
(true, 0x1B5) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// SystemEdit ($A9C2)
// Per IM:I I-441: "Call SystemEdit when there's a
// mouse-down event in the menu bar and the user chooses
// one of the five standard editing commands from the
// Edit menu. ... If the active window does not belong
// to a desk accessory ... SystemEdit returns FALSE so
// that your application will perform the editing
// function on its own document."
// FUNCTION SystemEdit(editCmd: INTEGER): BOOLEAN;
// Inside Macintosh Volume I, I-441
//
// Calling convention (Tool-bit FUNCTION per IM:I I-441):
// Stack on entry: SP+0 = editCmd INTEGER (2 bytes),
// SP+2 = BOOLEAN result placeholder
// (2 bytes, pre-pushed by caller).
// Trap pops the 2-byte editCmd and writes the BOOLEAN
// result to [SP+0] after pop (i.e. the original SP+2
// slot). Net stack effect after the caller's epilogue
// reads the result is zero — A7 returns to its pre-call
// value (engines-agree per Pascal FUNCTION calling
// convention).
//
// Standard editCmd values per the IM:I I-441 table:
// 0 undoCmd
// 2 cutCmd
// 3 copyCmd
// 4 pasteCmd
// 5 clearCmd
// (1 is a historic gap.)
//
// MPW Universal Headers Desk.h:
// EXTERN_API(Boolean) SystemEdit(short editCmd)
// ONEWORDINLINE(0xA9C2);
//
// Assembly-language note (IM:I I-441): "The macro you
// invoke to call SystemEdit from assembly language is
// named _SysEdit." — same trap word ($A9C2), MPW glue
// just reuses the alias.
//
// HLE compromise: Systemless models no Desk Accessories so
// no DA window is ever active. Per IM:I I-441 the FALSE
// return is the documented "no DA wants this; app should
// perform the edit on its own document" path — corpus
// apps' Cut/Copy/Paste menu handlers correctly fall
// through to their own document-editing code.
//
// Engines-agree subset (per a9b4_a9c2_systemtask_systemedit_strict):
// - Pascal FUNCTION calling convention with trap-side
// 2-byte editCmd pop; A7 returns to its pre-call value
// - BOOLEAN result == FALSE (0) for every standard
// editCmd value (0/2/3/4/5) on the no-DA-owns-active-
// window path
//
// Catalogue-proof: a9b4_a9c2_systemtask_systemedit_strict
// B2: A9C2:systemedit_consumes_editcmd_and_returns_false_boolean_result
//
// Contract tests:
// - systemedit_consumes_editcmd_and_returns_false_boolean_result (copyCmd)
// - systemedit_returns_false_for_every_standard_editcmd
(true, 0x1C2) => {
let sp = cpu.read_reg(Register::A7);
bus.write_word(sp + 2, 0); // return FALSE
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// ========== Scrap Manager ==========
// InfoScrap ($A9F9)
// Returns a pointer to a ScrapStuff record describing the desk scrap.
// FUNCTION InfoScrap: PScrapStuff;
// Inside Macintosh Volume I, I-457
//
// Regression coverage:
// a9f9_infoscrap_strict
// src/trap/toolbox.rs::tests::infoscrap_reports_in_memory_scrapstate_and_entry_size
// src/trap/toolbox.rs::tests::infoscrap_scraphandle_serializes_current_entries
//
// InfoScrap ($A9F9): Returns pointer to ScrapStuff record
// (scrapSize, scrapHandle, scrapCount, scrapState, scrapName)
// and exposes a live in-memory desk-scrap handle when
// scrapState is positive per IM:I I-457. When the scrap
// has been unloaded, scrapHandle is NIL and scrapState is 0
// until LoadScrap/ZeroScrap marks it resident again.
(true, 0x1F9) => {
let sp = cpu.read_reg(Register::A7);
// Allocate ScrapStuff at a fixed location if not yet done
let scrap_stuff_ptr = self.scrap_stuff_ptr.get_or_insert_with(|| bus.alloc(16));
let ptr = *scrap_stuff_ptr;
let total_size = self.serialized_scrap_size();
let scrap_handle = if self.scrap_in_memory {
self.sync_scrap_handle(bus)
} else {
0
};
bus.write_long(ptr, total_size); // scrapSize
bus.write_long(ptr + 4, scrap_handle); // scrapHandle (live in-memory desk scrap)
bus.write_word(ptr + 8, self.scrap_count as u16); // scrapCount
// IM:I I-457: scrapState is positive when the scrap is in memory.
bus.write_word(ptr + 10, if self.scrap_in_memory { 1 } else { 0 });
bus.write_long(ptr + 12, 0); // scrapName (NIL)
bus.write_long(sp, ptr); // return value
Ok(())
}
// UnloadScrap ($A9FA)
// Writes the desk scrap from memory to the scrap file and
// releases the memory it occupied.
// FUNCTION UnloadScrap : LONGINT;
// Inside Macintosh Volume I (1985), p. I-458.
//
// Tool Trap (bit 11 of the trap word is set) with Pascal
// calling convention: 0 argument bytes, 4-byte LONGINT
// OSStatus result written to [SP+0]. MPW Universal Headers
// Scrap.h:
// EXTERN_API(OSStatus) UnloadScrap(void) ONEWORDINLINE(0xA9FA);
// The assembly macro name is `_UnlodeScrap` per IM:I I-458
// (legacy Pascal-source spelling); the trap word $A9FA is
// unchanged across spellings.
//
// Per IM:I I-458 the documented success path is:
// "If the desk scrap is already on the disk, UnloadScrap
// does nothing. If no error occurs, UnloadScrap returns
// the result code noErr".
//
// Systemless HLE models the observable resident/on-disk
// transition: the in-memory scrap handle is dropped and
// InfoScrap reports scrapState=0 until LoadScrap brings it
// back. `scrap_clipboard_writable` gates the observable
// error path when the scrap is resident but cannot be
// written out.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::unloadscrap_and_loadscrap_return_noerr
(true, 0x1FA) => {
let sp = cpu.read_reg(Register::A7);
if self.scrap_in_memory && !self.scrap_clipboard_writable {
bus.write_long(sp, (-1i32) as u32); // generic non-zero OSErr
} else if self.scrap_in_memory {
self.scrap_in_memory = false;
self.scrap_handle_dirty = true;
if let Some(handle) = self.scrap_handle.take() {
let _ = self.write_bytes_to_handle(bus, handle, &[]);
bus.free(handle);
}
bus.write_long(sp, 0); // noErr (Systemless HLE: no scrap-file IO)
} else {
bus.write_long(sp, 0); // already on disk; noErr
}
Ok(())
}
// LoadScrap ($A9FB)
// Reads the desk scrap from the scrap file into memory.
// FUNCTION LoadScrap : LONGINT;
// Inside Macintosh Volume I (1985), p. I-458.
//
// Tool Trap (bit 11 of the trap word is set) with Pascal
// calling convention: 0 argument bytes, 4-byte LONGINT
// OSStatus result written to [SP+0]. MPW Universal Headers
// Scrap.h:
// EXTERN_API(OSStatus) LoadScrap(void) ONEWORDINLINE(0xA9FB);
// The assembly macro name is `_LodeScrap` per IM:I I-458
// (legacy Pascal-source spelling); the trap word $A9FB is
// unchanged.
//
// Per IM:I I-458 the documented success path is:
// "If the desk scrap is already in memory, it does
// nothing. If no error occurs, LoadScrap returns the
// result code noErr".
//
// Systemless HLE marks the scrap resident again after an
// unload so InfoScrap can lazily recreate the in-memory
// handle on demand. On a freshly booted system the scrap
// is already resident, so the nominal noErr path remains
// an in-memory no-op.
//
// Witnessed by:
// a9fb_loadscrap_strict
// (A9FB:loadscrap_returns_noerr_when_no_error)
// src/trap/toolbox.rs::tests::unloadscrap_and_loadscrap_return_noerr
// src/trap/toolbox.rs::tests::loadscrap_writes_noerr_to_pascal_function_result_slot_and_preserves_stack_pointer
(true, 0x1FB) => {
let sp = cpu.read_reg(Register::A7);
if !self.scrap_in_memory {
self.scrap_in_memory = true;
self.scrap_handle_dirty = true;
}
bus.write_long(sp, 0); // noErr
Ok(())
}
// ZeroScrap ($A9FC)
// Clears the desk scrap and increments the scrap change count.
// FUNCTION ZeroScrap: LONGINT;
// Inside Macintosh Volume I, I-458
//
// Regression coverage:
// src/trap/toolbox.rs::tests::zeroscrap_clears_contents_and_changes_scrapcount
// src/trap/toolbox.rs::tests::infoscrap_reports_in_memory_scrapstate_and_entry_size
// ZeroScrap ($A9FC): Clears scrap entries and increments scrap_count per IM:I I-458
(true, 0x1FC) => {
let sp = cpu.read_reg(Register::A7);
self.scrap_entries.clear();
self.scrap_count = self.scrap_count.wrapping_add(1);
self.scrap_in_memory = true;
self.scrap_handle_dirty = true;
bus.write_long(sp, 0); // noErr
Ok(())
}
// GetScrap ($A9FD)
// Reads data of the specified type from the desk scrap.
// FUNCTION GetScrap(hDest: Handle; theType: ResType; VAR offset: LONGINT): LONGINT;
// Inside Macintosh Volume I, I-458
//
// Returns the length of the data (positive) on success, or a negative
// error code. If hDest is NIL (0), returns the size and offset without
// copying data. If the requested type is not found, returns noTypeErr (-102).
//
// Regression coverage:
// src/trap/toolbox.rs::tests::getscrap_missing_type_returns_notypeerr
// src/trap/toolbox.rs::tests::getscrap_with_nil_handle_returns_length_and_data_offset
// src/trap/toolbox.rs::tests::getscrap_duplicate_type_returns_first_occurrence
// src/trap/toolbox.rs::tests::getscrap_existing_handle_resizes_copy_and_preserves_ownership
// GetScrap ($A9FD): Reads scrap data by type; supports NIL handle query; returns noTypeErr (-102) if not found per IM:I I-458
(true, 0x1FD) => {
let sp = cpu.read_reg(Register::A7);
let offset_ptr = bus.read_long(sp); // VAR offset: LONGINT
let the_type = bus.read_long(sp + 4).to_be_bytes(); // theType: ResType
let h_dest = bus.read_long(sp + 8); // hDest: Handle
// Search scrap for matching type. Per IM:I-459 offset is
// the byte offset of the DATA (not the entry header) from
// the start of the scrap. Each entry is laid out as:
// type(4) + length(4) + data + pad-to-even. So for entry
// N the data offset is sum(8 + padded_len_i for i<N) + 8.
let mut found_offset: u32 = 0;
let mut found = None;
for entry in &self.scrap_entries {
if entry.0 == the_type {
found = Some(entry.1.clone());
found_offset += 8; // skip the matched entry's own header
break;
}
// Offset accounts for type(4) + length(4) + data (padded to even)
let padded_len = (entry.1.len() as u32 + 1) & !1;
found_offset += 8 + padded_len;
}
match found {
Some(data) => {
let data_len = data.len() as u32;
// Write offset
if offset_ptr != 0 {
bus.write_long(offset_ptr, found_offset);
}
// If hDest is not NIL, copy data into it
if h_dest != 0
&& self.write_bytes_to_handle(bus, h_dest, &data) == 0
&& data_len != 0
{
bus.write_long(sp + 12, (-108i32) as u32); // memFullErr
cpu.write_reg(Register::A7, sp + 12);
return Some(Ok(()));
}
// Return length (positive = success)
bus.write_long(sp + 12, data_len);
}
None => {
// noTypeErr = -102
bus.write_long(sp + 12, (-102i32) as u32);
}
}
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// PutScrap ($A9FE)
// Writes data of the specified type to the desk scrap.
// FUNCTION PutScrap(length: LONGINT; theType: ResType; source: Ptr): LONGINT;
// Inside Macintosh Volume I, I-459
//
// Must be called after ZeroScrap. Appends data of the given type.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::infoscrap_reports_in_memory_scrapstate_and_entry_size
// src/trap/toolbox.rs::tests::getscrap_duplicate_type_returns_first_occurrence
// PutScrap ($A9FE): Appends type+data to scrap_entries per IM:I I-459
(true, 0x1FE) => {
let sp = cpu.read_reg(Register::A7);
let source = bus.read_long(sp); // source: Ptr
let the_type = bus.read_long(sp + 4).to_be_bytes(); // theType: ResType
let length = bus.read_long(sp + 8) as i32; // length: LONGINT
if length > 0 && source != 0 {
let mut data = vec![0u8; length as usize];
for (i, byte) in data.iter_mut().enumerate() {
*byte = bus.read_byte(source + i as u32);
}
self.scrap_entries.push((the_type, data));
self.scrap_handle_dirty = true;
}
bus.write_long(sp + 12, 0); // noErr
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// ========== Resource Manager extras ==========
// SetResPurge ($A993)
// Installs or removes a Memory Manager hook that writes modified
// resources to disk before purging.
// PROCEDURE SetResPurge(install: BOOLEAN);
// Inside Macintosh Volume I, I-126
//
// Regression coverage:
// tests::setrespurge_consumes_boolean_argument
// tests::setrespurge_toggles_resource_purge_install_flag
// SetResPurge ($A993): Stores install flag in res_purge per IM:I I-126
(true, 0x193) => {
let sp = cpu.read_reg(Register::A7);
// MPW passes a Pascal Boolean in the high byte of this
// stack word. The low byte is padding and can be non-zero;
// reading the whole word would turn SetResPurge(FALSE) into
// TRUE.
let install = (bus.read_word(sp) >> 8) != 0;
self.res_purge = install;
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// SetResLoad ($A99B)
// Enables or disables automatic loading of resources.
// PROCEDURE SetResLoad(load: BOOLEAN);
// Inside Macintosh: More Macintosh Toolbox 1993, 1-79 to 1-80
//
// Regression coverage:
// setresload_toggles_autoload
// setresload_true_enables
// setresload_false_disables
// SetResLoad ($A99B): Stores load flag in res_load per MMTB 1-79; resource-returning helpers consume it to return empty handles until LoadResource.
(true, 0x19B) => {
let sp = cpu.read_reg(Register::A7);
// MPW passes a Pascal Boolean in the high byte of this
// stack word. The low byte is padding and can be non-zero;
// reading the whole word turns SetResLoad(FALSE) into TRUE.
let load = (bus.read_word(sp) >> 8) != 0;
self.res_load = load;
// Clear ResErr on success — real ROM does, and callers
// that probe ResError after a successful SetResLoad
// otherwise see stale values from boot-time auto-loads.
bus.write_word(0x0A60, 0);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// GetIndResource ($A99D) and Get1IndResource ($A80E)
// FUNCTION GetIndResource (theType: ResType; index: INTEGER): Handle;
// FUNCTION Get1IndResource (theType: ResType; index: INTEGER): Handle;
// Inside Macintosh Volume I, I-116; Volume IV, IV-14 to IV-15.
//
// The two traps share a Pascal signature but differ on which
// resource files they walk:
// $A99D GetIndResource — full search chain (current file + all
// files opened before it).
// $A80E Get1IndResource — current resource file only. The
// assembly macro is _Get1IxResource;
// see IM:IV-15 "Assembly-language note".
//
// Aliasing them onto the full-chain implementation silently
// over-counts in multi-file scenarios — the regression flagged
// by the previous Ralph iteration. Keep them separate.
// GetIndResource ($A99D): Walks the full resource search chain by type, returns Nth resource handle (1-based) per IM:I I-116; maybe_inject_ajcp_decompress runs for POD's compressed resources
(true, 0x19D) => self.handle_get_ind_resource(bus, cpu, false),
// Get1IndResource ($A80E): Returns Nth resource of theType in the CURRENT resource file only (assembly name _Get1IxResource) per IM:IV-15
(true, 0x00E) => self.handle_get_ind_resource(bus, cpu, true),
// CountTypes ($A99E)
// Returns the number of unique resource types across all open resource files.
// FUNCTION CountTypes: INTEGER;
// Inside Macintosh Volume I, I-117
//
// Regression coverage:
// counttypes_returns_type_count
// counttypes_returns_zero_with_no_resources
// CountTypes ($A99E): Returns count of unique resource types across all open resource files per IM:I I-117
(true, 0x19E) => {
let sp = cpu.read_reg(Register::A7);
let count = if let Some(ref resources) = self.resources {
let mut types = std::collections::HashSet::new();
for refnum in self.resource_search_order() {
if let Some(file) = resources.files.get(&refnum) {
for (res_type, _) in file.loaded.keys() {
types.insert(*res_type);
}
}
}
types.len() as u16
} else {
0
};
bus.write_word(sp, count);
Ok(())
}
// GetIndType ($A99F)
// Returns the Nth unique resource type from all open resource files.
// PROCEDURE GetIndType(VAR theType: ResType; index: INTEGER);
// Inside Macintosh Volume I, I-117
//
// Index is 1-based. If out of range, writes four NUL bytes.
//
// Regression coverage:
// getindtype_returns_type_by_index
// getindtype_out_of_range_returns_nul
// GetIndType ($A99F): Returns Nth unique resource type (1-based) via VAR theType ptr; writes four NUL bytes when index out of range per IM:I I-117
(true, 0x19F) => self.handle_get_ind_type(bus, cpu, false),
// Get1IndType ($A80F)
// Returns the Nth unique resource type in the CURRENT resource
// file only — the "1" sibling of GetIndType ($A99F) which spans
// the full open-file chain.
// PROCEDURE Get1IndType(VAR theType: ResType; index: INTEGER);
// Inside Macintosh Volume IV, IV-15
//
// Assembly-language note (IM:IV-15): the assembly macro is
// _Get1IxType, hence the otherwise-puzzling trap-word slot.
//
// Aliasing this onto $A99F silently leaks types from other
// open resource files into the index — see the regression-fix
// commit for Get1IndResource ($A80E) which addressed the
// identical bug for handles.
//
// Regression coverage:
// get1indtype_returns_nth_type_in_current_file
// get1indtype_out_of_range_returns_nul_bytes
// get1indtype_ignores_types_in_other_open_files
// Get1IndType ($A80F): Returns Nth unique resource type in current resource file only (assembly name _Get1IxType) per IM:IV-15.
(true, 0x00F) => self.handle_get_ind_type(bus, cpu, true),
// Count1Types ($A81C)
// Returns the number of unique resource types in the current resource
// file only — the "1" sibling of CountTypes ($A99E) which spans the
// whole open-resource-file chain.
// FUNCTION Count1Types: INTEGER;
// Inside Macintosh: More Macintosh Toolbox 1993, 1-102
//
// Stack frame (Pascal, no args, INTEGER result):
// SP+0 result slot (2 bytes, caller-allocated)
// Post-call SP is unchanged — the result word stays where the
// caller already reserved it.
//
// Regression coverage:
// count1types_returns_zero_with_no_resources_in_current_file
// count1types_counts_unique_types_in_current_file
// count1types_pops_no_args_leaves_word_result
// Count1Types ($A81C): Counts unique types in current resource file only per IM:MTb 1-102.
(true, 0x01C) => {
let sp = cpu.read_reg(Register::A7);
let count = if let Some(ref resources) = self.resources {
let refnum = self.current_resource_refnum();
resources
.files
.get(&refnum)
.map(|file| {
let mut types = std::collections::HashSet::new();
for (res_type, _) in file.loaded.keys() {
types.insert(*res_type);
}
types.len() as u16
})
.unwrap_or(0)
} else {
0
};
bus.write_word(sp, count);
Ok(())
}
// GetNamedResource ($A9A1)
// Returns a handle to the named resource, searching the resource chain.
// FUNCTION GetNamedResource(theType: ResType; name: Str255): Handle;
// More Macintosh Toolbox 1993, 1-75
// GetNamedResource ($A9A1): Searches the resource chain by Pascal name string
(true, 0x1A1) => {
let sp = cpu.read_reg(Register::A7);
let name_ptr = bus.read_long(sp);
let raw_res_type = bus.read_long(sp + 4).to_be_bytes();
let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
let type_str = std::str::from_utf8(&res_type).unwrap_or("????");
let name_len = bus.read_byte(name_ptr) as usize;
let mut name_bytes = vec![0u8; name_len];
for (i, byte) in name_bytes.iter_mut().enumerate() {
*byte = bus.read_byte(name_ptr + 1 + i as u32);
}
let name = String::from_utf8_lossy(&name_bytes).to_string();
eprintln!("[TRAP] GetNamedResource('{}', \"{}\")", type_str, name);
let handle =
self.find_named_resource_any(res_type, &name)
.map(|(refnum, id, ptr)| {
self.get_or_create_resource_handle_in_file(
bus, res_type, id, ptr, refnum,
)
});
if let Some(handle) = handle {
eprintln!("[TRAP] GetNamedResource -> handle ${:08X}", handle);
bus.write_word(0x0A60, 0); // ResErr = noErr
cpu.write_reg(Register::D0, 0);
bus.write_long(sp + 8, handle);
cpu.write_reg(Register::A7, sp + 8);
self.maybe_inject_ajcp_decompress(bus, cpu, handle);
} else {
eprintln!("[TRAP] GetNamedResource -> NULL (not found)");
bus.write_word(0x0A60, (-192i16) as u16); // ResErr = resNotFound
cpu.write_reg(Register::D0, (-192i32) as u32);
bus.write_long(sp + 8, 0);
cpu.write_reg(Register::A7, sp + 8);
}
Ok(())
}
// SetResAttrs ($A9A7)
// Sets the resource attributes for a resource. The resProtected
// attribute takes effect immediately; others take effect next read.
// WARNING: Do not use SetResAttrs to set resChanged — use
// ChangedResource instead.
// PROCEDURE SetResAttrs(theResource: Handle; attrs: INTEGER);
// Inside Macintosh Volume I, I-122
//
// Pascal arg push order (left-to-right): theResource is pushed
// first (deeper on stack), attrs second (shallower):
// SP+0: attrs (2)
// SP+2: theResource handle (4)
// SetResAttrs ($A9A7): Sets resource attributes in memory map per IM:I I-122; resProtected takes effect immediately
(true, 0x1A7) => {
let sp = cpu.read_reg(Register::A7);
let new_attrs = bus.read_word(sp) as u8;
let handle = bus.read_long(sp + 2);
if let Some((refnum, res_type, res_id)) = self.resource_record_for_handle(handle) {
if let Some(resources) = self.resources.as_mut() {
if let Some(file) = resources.files.get_mut(&refnum) {
file.attrs.insert((res_type, res_id), new_attrs);
}
}
bus.write_word(0x0A60, 0); // noErr
} else {
bus.write_word(0x0A60, super::TrapDispatcher::RES_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
// RmveResource ($A9AD)
// Removes the resource reference from the current resource file's
// map. The data is NOT freed — call DisposHandle separately.
// Does nothing and returns rmvResFailed if the resource is
// protected or not in the current resource file.
// PROCEDURE RmveResource(theResource: Handle);
// Inside Macintosh Volume I, I-124
//
// Regression coverage:
// rmveresource_removes_resource_from_map
// rmveresource_invalid_handle_sets_reserr
// rmveresource_protected_resource_fails
// rmveresource_pops_four_bytes
// RmveResource ($A9AD): Removes resource reference from current file map; respects resProtected per IM:I I-124
(true, 0x1AD) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
self.remove_resource_reference(bus, handle);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// UniqueID ($A9C1)
// Returns a resource ID > 0 not assigned to any resource of the given type.
// FUNCTION UniqueID(theType: ResType): INTEGER;
// Inside Macintosh Volume I, I-121
//
// Regression coverage:
// uniqueid_pair_returns_128_when_no_resources_loaded_parametric
// uniqueid_is_use_res_file_independent_while_unique1id_restricts_to_current_file
// uniqueid_pair_skips_contiguous_run_returns_first_gap_parametric
// uniqueid_pair_returns_128_for_unknown_type_with_other_types_loaded_parametric
// uniqueid_pair_does_not_mutate_other_registers_or_caller_stack_parametric
// uniqueid_returns_unused_id
// uniqueid_avoids_existing_ids
// UniqueID ($A9C1): Scans all open files for used IDs (USE_RES_FILE_INDEPENDENT per IM:I I-121 "any open resource file"); returns unused ID >= 128
(true, 0x1C1) => self.handle_unique_id(bus, cpu, false),
// Unique1ID ($A810)
// Returns a resource ID > 0 not assigned to any resource of the
// given type in the CURRENT resource file only — the "1" sibling
// of UniqueID ($A9C1) which scans every open resource file.
// FUNCTION Unique1ID(theType: ResType): INTEGER;
// Inside Macintosh Volume IV, IV-16
//
// Aliasing this onto $A9C1 silently makes the chain's IDs
// collide with the current-file uniqueness check — see the
// regression-fix commits for Get1IndResource ($A80E) and
// Get1IndType ($A80F) which addressed the analogous bugs for
// handles and types.
//
// Regression coverage:
// uniqueid_pair_returns_128_when_no_resources_loaded_parametric
// uniqueid_is_use_res_file_independent_while_unique1id_restricts_to_current_file
// uniqueid_pair_skips_contiguous_run_returns_first_gap_parametric
// uniqueid_pair_returns_128_for_unknown_type_with_other_types_loaded_parametric
// uniqueid_pair_does_not_mutate_other_registers_or_caller_stack_parametric
// unique1id_generates_id_in_current_file
// unique1id_avoids_ids_in_current_file
// unique1id_ignores_ids_in_other_open_files
// Unique1ID ($A810): Scans current resource file only for used IDs; returns unused ID >= 128 per IM:IV IV-16.
(true, 0x010) => self.handle_unique_id(bus, cpu, true),
// RsrcMapEntry ($A9C5)
// FUNCTION RsrcMapEntry(theResource: Handle): LONGINT;
// Params: 4, returns 4
// RsrcMapEntry ($A9C5): Returns the reference-record offset from
// the start of the resource map for live resource handles; NIL
// and non-resource handles leave the prior result in place and
// report resNotFound per BasiliskII / IM:IV IV-16 / More
// Macintosh Toolbox 1993 1-120.
(true, 0x1C5) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
let result = self.rsrc_map_entry_for_handle(handle);
if let Some(offset) = result {
bus.write_long(sp + 4, offset);
bus.write_word(0x0A60, 0);
} else {
bus.write_word(0x0A60, super::TrapDispatcher::RES_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// UpdateResFile ($A999)
// Writes all changed/added/removed resources and the resource
// map to the resource file. In Systemless's HLE, clears resChanged
// on all resources to simulate a successful flush.
// PROCEDURE UpdateResFile(refNum: INTEGER);
// Inside Macintosh Volume I, I-124
//
// Regression coverage:
// updateresfile_clears_changed_flags
// updateresfile_invalid_refnum_sets_reserr
// updateresfile_pops_two_bytes
// UpdateResFile ($A999): Validates refnum; clears resChanged on all resources in file per IM:I I-124
(true, 0x199) => {
let sp = cpu.read_reg(Register::A7);
let refnum = bus.read_word(sp);
const RES_F_NOT_FOUND: i16 = -193;
let file_exists = self
.resources
.as_ref()
.is_some_and(|r| r.files.contains_key(&refnum));
if file_exists {
// Clear resChanged on all resources in this file
if let Some(resources) = self.resources.as_mut() {
if let Some(file) = resources.files.get_mut(&refnum) {
for attr in file.attrs.values_mut() {
*attr &= !(super::TrapDispatcher::RES_CHANGED_ATTR as u8);
}
file.map_attrs &= !super::TrapDispatcher::RES_MAP_CHANGED_ATTR;
}
}
bus.write_word(0x0A60, 0); // noErr
} else {
bus.write_word(0x0A60, RES_F_NOT_FOUND as u16);
}
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// InitResources ($A995)
// FUNCTION InitResources: INTEGER;
// InitResources ($A995): BasiliskII returns -1 on the nominal
// startup path; this matches the observable result value in the
// public fixture, while the HLE does not model the resource-file
// boot choreography from the original Toolbox init sequence.
(true, 0x195) => {
let sp = cpu.read_reg(Register::A7);
bus.write_word(sp, (-1i16) as u16);
Ok(())
}
// RsrcZoneInit ($A996)
// PROCEDURE RsrcZoneInit;
// RsrcZoneInit ($A996): No resource zone is allocated per IM:I I-114
(true, 0x196) => Ok(()),
// HOpenResFile ($A81A)
// FUNCTION HOpenResFile(vRefNum: Integer; dirID: LongInt;
// fileName: Str255;
// permission: SignedByte): Integer;
// Inside Macintosh Volume VI, page 13-19 (Resource Manager —
// HFS variant of OpenRFPerm $A9C4 / OpenResFile $A997).
//
// Stack frame (Pascal, args pushed left-to-right; FUNCTION
// result slot pre-pushed by caller, deepest):
// sp+0 permission INTEGER (2; SignedByte in low byte)
// sp+2 fileName Str255 ptr (4)
// sp+6 dirID LongInt (4)
// sp+10 vRefNum INTEGER (2)
// sp+12 result INTEGER (2 — refnum or -1)
// Pop 12 bytes; result lands at the new SP.
//
// Systemless's flat VFS has no per-volume / per-directory
// namespace, so vRefNum and dirID are ignored — the file
// is resolved by name alone, identical to the existing
// HCreateResFile $A81B path. Behaviour otherwise mirrors
// OpenRFPerm: dedup re-opens against `loaded_files`,
// return the existing refnum, and leave current file unchanged
// on already-open paths per MTb 1993 1-63. ResErr is noErr on
// hit and fnfErr (-43) on miss (MTb 1993 1-64 result table).
// HOpenResFile ($A81A): HFS variant of OpenRFPerm; vRefNum/dirID
// ignored in flat VFS. New open sets current file; already-open
// path returns existing refnum without switching current.
(true, 0x01A) => {
let sp = cpu.read_reg(Register::A7);
let _perm = bus.read_byte(sp) as i8 as i16;
let name_ptr = bus.read_long(sp + 2);
let _dir_id = bus.read_long(sp + 6);
let _v_ref = bus.read_word(sp + 10) as i16;
let name = if name_ptr != 0 {
String::from_utf8_lossy(&bus.read_pstring(name_ptr)).into_owned()
} else {
String::new()
};
if super::dispatch::trace_resfile_enabled() {
eprintln!("[TRAP] HOpenResFile(\"{}\")", name);
}
if let Some(vfs_key) = self.find_vfs_rsrc_file(&name) {
if let Some(existing) = self.refnum_for_resource_file_name(&vfs_key) {
if super::dispatch::trace_resfile_enabled() {
eprintln!(
"[TRAP] HOpenResFile: \"{}\" already open as refnum {}, dedup",
name, existing
);
}
bus.write_word(sp + 12, existing);
bus.write_word(0x0A60, 0); // ResErr = noErr
cpu.write_reg(Register::A7, sp + 12);
return Some(Ok(()));
}
let refnum = self.open_resource_file_from_vfs_key(bus, &vfs_key, false);
bus.write_word(sp + 12, refnum);
bus.write_word(0x0A60, 0); // ResErr = noErr
} else {
bus.write_word(sp + 12, (-1i16) as u16);
bus.write_word(0x0A60, (-43i16) as u16); // fnfErr
}
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// HCreateResFile ($A81B)
// PROCEDURE HCreateResFile(vRefNum: Integer; dirID: LongInt;
// fileName: Str255);
// Inside Macintosh Volume VI, page 9-13 (Files: Volumes section);
// Inside Macintosh Volume IV, IV-148; IM:VI 57521.
//
// Adds an empty resource fork to an existing file. Stack:
// sp+0 fileName StringPtr (4)
// sp+4 dirID (4)
// sp+8 vRefNum (2)
// No result. Pops 10 bytes. Matches the standard "PBCreate then
// HCreateResFile" pattern used by titles preparing a key/prefs
// file (e.g. Meteor Storm's MS UserKey).
//
// Systemless models the data fork as `vfs[name]` and the resource
// fork as `vfs_rsrc[name]`. Per MMTB 1-56, HCreateResFile also
// creates the file when it is missing: the data fork is zero
// length and the resource fork contains an empty resource map.
// HCreateResFile ($A81B): Creates missing file plus empty resource
// fork, or returns dupFNErr when a non-empty resource fork already exists.
(true, 0x01B) => {
let sp = cpu.read_reg(Register::A7);
let name_ptr = bus.read_long(sp);
let _dir_id = bus.read_long(sp + 4);
let _v_ref = bus.read_word(sp + 8) as i16;
let name = if name_ptr != 0 {
String::from_utf8_lossy(&bus.read_pstring(name_ptr)).into_owned()
} else {
String::new()
};
if super::dispatch::trace_resfile_enabled() {
eprintln!("[TRAP] HCreateResFile(\"{}\")", name);
}
if name.is_empty() {
bus.write_word(0x0A60, (-37i16) as u16); // bdNamErr
} else if self.find_vfs_rsrc_file(&name).is_some() {
bus.write_word(0x0A60, (-48i16) as u16); // dupFNErr
} else {
let vfs_key = self
.find_vfs_file(&name)
.unwrap_or_else(|| Self::normalize_vfs_path(&name));
self.vfs.entry(vfs_key.clone()).or_default();
self.vfs_rsrc.entry(vfs_key.clone()).or_default();
self.touch_vfs_entry(&vfs_key);
if let Some(ref dir) = self.output_dir {
let host_path = dir.join(&vfs_key);
if let Some(parent) = host_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(host_path, []);
}
bus.write_word(0x0A60, 0); // ResErr = noErr
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// OpenResFile ($A997)
// FUNCTION OpenResFile(fileName: Str255): INTEGER;
// Params: 4, returns 2
// OpenResFile ($A997): Opens VFS resource fork by name; dedup'd refnum on re-open (logged via SYSTEMLESS_TRACE_RESFILE) per IM:I I-115
(true, 0x197) => {
let sp = cpu.read_reg(Register::A7);
let name_ptr = bus.read_long(sp);
if name_ptr != 0 {
let bytes = bus.read_pstring(name_ptr);
let name = String::from_utf8_lossy(&bytes);
if super::dispatch::trace_resfile_enabled() {
eprintln!("[TRAP] OpenResFile(\"{}\")", name);
}
// Try to load resource fork
if let Some(vfs_key) = self.find_vfs_rsrc_file(&name) {
// Dedupe (see OpenRFPerm above for rationale).
if let Some(existing) = self.refnum_for_resource_file_name(&vfs_key) {
if super::dispatch::trace_resfile_enabled() {
eprintln!(
"[TRAP] OpenResFile: \"{}\" already open as refnum {}, dedup",
name, existing
);
}
bus.write_word(sp + 4, existing);
cpu.write_reg(Register::D0, existing as u32);
bus.write_word(0x0A60, 0); // ResErr = noErr
// IM:I p. I-115: already-open OpenResFile returns
// the existing refnum but does not make that file
// the current resource file.
cpu.write_reg(Register::A7, sp + 4);
return Some(Ok(()));
}
let refnum = self.open_resource_file_from_vfs_key(bus, &vfs_key, false);
bus.write_word(sp + 4, refnum);
cpu.write_reg(Register::D0, refnum as u32);
bus.write_word(0x0A60, 0); // ResErr = noErr
cpu.write_reg(Register::A7, sp + 4);
return Some(Ok(()));
}
}
// Not found — return -1
bus.write_word(sp + 4, (-1i16) as u16);
cpu.write_reg(Register::D0, (-1i32) as u32);
bus.write_word(0x0A60, (-43i16) as u16); // fnfErr
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// RmveReference ($A9AE) — obsolete alias for RemoveResource
// PROCEDURE RmveReference(theResource: Handle);
// RmveReference ($A9AE): Obsolete alias for RemoveResource; shares the RmveResource semantics and pops 4 bytes
(true, 0x1AE) => {
let sp = cpu.read_reg(Register::A7);
let handle = bus.read_long(sp);
self.remove_resource_reference(bus, handle);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// KeyTrans ($A9C3)
// FUNCTION KeyTrans(transData: Ptr; keycode: INTEGER;
// VAR state: LongInt): LongInt;
// Inside Macintosh: Macintosh Toolbox Essentials (1992), 2-110..2-111
// Inside Macintosh: Text (1993), C-19..C-20
//
// Stack: SP+0 state_ptr(4), SP+4 keycode(2), SP+6 transData(4),
// SP+10 result(4).
//
// Systemless's key handling converts key codes to ASCII/event
// codes elsewhere. For callers that reach this trap we now
// honor the caller-supplied KCHR layout on the nominal
// non-dead-key path instead of fabricating the low byte of
// the raw keycode.
//
// Regression coverage:
// keytrans_consumes_state_keycode_transdata_arguments_and_writes_long_result_slot
// keytrans_single_character_result_uses_charcode2_low_byte
// keytrans_non_deadkey_path_clears_state_for_followup_calls
// KeyTrans ($A9C3): Uses the caller's KCHR mapping table on
// the nominal path and clears the pending state for the next
// call once a character has been translated.
(true, 0x1C3) => {
let sp = cpu.read_reg(Register::A7);
let state_ptr = bus.read_long(sp);
let keycode = bus.read_word(sp + 4);
let trans_data = bus.read_long(sp + 6);
let result = Self::keytrans_lookup_character(bus, trans_data, keycode);
if state_ptr != 0 {
// IM:Text C-19..C-20: state carries dead-key context only when
// a dead-key path is active; the nominal non-dead-key path has
// no pending state for the next call.
bus.write_long(state_ptr, 0);
}
bus.write_long(sp + 10, result);
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// PutIcon ($A9CA)
// Undocumented/internal trap. The BasiliskII-backed runtime proof
// only witnesses that the caller stack is preserved, so keep this
// as a conservative no-op.
// Regression coverage:
// src/trap/toolbox.rs::tests::puticon_preserves_a7
// PutIcon ($A9CA): Preserves A7 (undocumented internal trap)
(true, 0x1CA) => Ok(()),
// ========== Date/Time ==========
// Secs2Date / SecondsToDate ($A9C6)
// Converts seconds since Jan 1, 1904 to a DateTimeRec.
// PROCEDURE Secs2Date(secs: LONGINT; VAR date: DateTimeRec);
// Register convention: D0 = secs (input), A0 = DateTimeRec ptr (output)
// Inside Macintosh Volume II, II-379
// Secs2Date / SecondsToDate ($A9C6): Register convention: D0=secs (input), A0=DateTimeRec ptr (output); full Gregorian calendar conversion from Mac epoch (Inside Macintosh Volume II, II-379)
(true, 0x1C6) => {
let secs = cpu.read_reg(Register::D0);
let date_ptr = cpu.read_reg(Register::A0);
if date_ptr != 0 {
let (year, month, day, hour, minute, second, day_of_week) = secs_to_date(secs);
bus.write_word(date_ptr, year); // year
bus.write_word(date_ptr + 2, month); // month
bus.write_word(date_ptr + 4, day); // day
bus.write_word(date_ptr + 6, hour); // hour
bus.write_word(date_ptr + 8, minute); // minute
bus.write_word(date_ptr + 10, second); // second
bus.write_word(date_ptr + 12, day_of_week); // dayOfWeek
if trace_entropy_enabled() {
eprintln!(
"[ENTROPY] Secs2Date pc=${:08X} secs={} -> {:04}-{:02}-{:02} {:02}:{:02}:{:02} dow={}",
cpu.read_reg(Register::PC),
secs,
year,
month,
day,
hour,
minute,
second,
day_of_week
);
}
}
Ok(())
}
// Date2Secs / DateToSeconds ($A9C7)
// Converts a DateTimeRec to seconds since Jan 1, 1904.
// PROCEDURE Date2Secs(date: DateTimeRec; VAR secs: LONGINT);
// Register convention: A0 = DateTimeRec ptr (input), D0 = secs (output)
// Inside Macintosh Volume II, II-379
// Date2Secs / DateToSeconds ($A9C7): Register convention: A0=DateTimeRec ptr (input), D0=secs (output); full Gregorian calendar conversion to Mac epoch (Inside Macintosh Volume II, II-379)
(true, 0x1C7) => {
let date_ptr = cpu.read_reg(Register::A0);
if date_ptr != 0 {
let year = bus.read_word(date_ptr) as u32;
let month = bus.read_word(date_ptr + 2) as u32;
let day = bus.read_word(date_ptr + 4) as u32;
let hour = bus.read_word(date_ptr + 6) as u32;
let minute = bus.read_word(date_ptr + 8) as u32;
let second = bus.read_word(date_ptr + 10) as u32;
let secs = date_to_secs(year, month, day, hour, minute, second);
cpu.write_reg(Register::D0, secs);
if trace_entropy_enabled() {
eprintln!(
"[ENTROPY] Date2Secs pc=${:08X} {:04}-{:02}-{:02} {:02}:{:02}:{:02} -> secs={}",
cpu.read_reg(Register::PC),
year,
month,
day,
hour,
minute,
second,
secs
);
}
} else {
cpu.write_reg(Register::D0, 0);
}
Ok(())
}
// SysError ($A9C9)
// PROCEDURE SysError(errorCode: INTEGER);
// Inside Macintosh Volume II (1985), pp. II-358 to II-359;
// Inside Macintosh: Operating System Utilities (1994),
// pp. 2-13 to 2-14.
// SysError stores the error code in DSErrCode ($0AF0), then
// displays the System Error dialog box and never returns to
// the caller on real Mac OS.
// The application NEVER returns from SysError on real Mac OS
// — control transfers to the system error handler, the user
// dismisses the dialog, and the app is killed.
//
// Halting the runner matches real-Mac semantics and surfaces
// the originating divergence (the SysError call itself) as
// the halt PC, instead of the consequence of running past it.
// SysError ($A9C9): Halts emulation (real Mac displays System Error dialog and kills the app); preserves trap PC for diagnostic per IM:II II-358
(true, 0x1C9) => {
let sp = cpu.read_reg(Register::A7);
let error_code = bus.read_word(sp) as i16;
bus.write_word(addr::DS_ERR_CODE, error_code as u16);
eprintln!(
"[TRAP] SysError({}) — halting (real Mac would display system error dialog)",
error_code
);
cpu.write_reg(Register::A7, sp + 2);
Err(crate::Error::Halted)
}
// ========== Misc Window/Font/String ==========
// DrawGrowIcon ($A904)
// PROCEDURE DrawGrowIcon(theWindow: WindowPtr);
// Draws the grow icon (size box) in the bottom-right corner of the window.
// Inside Macintosh Volume I, I-296
// DrawGrowIcon ($A904): Draws size-box at window's bottom-right corner per IM:I I-296
(true, 0x104) => {
let sp = cpu.read_reg(Register::A7);
let window_ptr = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
self.draw_grow_icon(bus, window_ptr);
Ok(())
}
// DragGrayRgn ($A905)
// FUNCTION DragGrayRgn(theRgn: RgnHandle; startPt: Point;
// limitRect, slopRect: Rect;
// axis: INTEGER;
// actionProc: ProcPtr): LongInt;
// Inside Macintosh Volume I, I-302 (Window Manager); also IM:V V-201;
// Macintosh Toolbox Essentials 1992 4-95.
//
// Macro-aliased to $A926 DragTheRgn per IM:I I-93 explicit table:
// "DragGrayRgn | _DragGrayRgn or, after setting the global
// variable DragPattern, _DragTheRgn"
// Both trap words map to the same Pascal Toolbox routine; the
// only difference is _DragTheRgn lets you use a custom outline
// pattern via the DragPattern low-mem global. Pascal frame is
// identical and so is the pop count.
//
// Pascal frame (Rect args BY POINTER per IM:I-91 PEA convention,
// mirroring the Macintosh Toolbox Essentials 1992 4-95 sample
// assembly that pushes both Rects via PEA):
// sp+0 actionProc: ProcPtr (4)
// sp+4 axis: INTEGER (2)
// sp+6 slopRect ptr (4)
// sp+10 limitRect ptr (4)
// sp+14 startPt: Point (4)
// sp+18 theRgn: RgnHandle (4)
// sp+22 4-byte LONGINT result slot (caller pre-pushed)
// Total args = 22 bytes; pop = 22.
//
// HLE compromise + family rationale: see the Window Manager
// interactive-tracking family block above $A925 DragWindow at
// src/trap/window.rs (TrackGoAway / DragWindow / GrowWindow /
// DragTheRgn / DragGrayRgn — all share the "no WaitMouseUp /
// no DragHook dispatch" no-op shape).
//
// Result sentinel: $80008000 (high word $8000 + low word $8000)
// per IM:I I-302: "If the user releases the mouse button outside
// slopRect, DragGrayRgn returns $80008000". Systemless's HLE has
// no slopRect hit-test, so the "user released outside slopRect"
// sentinel is the appropriate semantic — apps that branch on
// result == $80008000 take the "no drag, leave the region in
// place" path which matches what real Mac does when the user
// gestured outside the bounds.
//
// Pop-count history note (load-bearing for future audits): an
// earlier iteration pinned this trap at pop=30, assuming Rect
// args by VALUE (4+4+8+8+2+4 = 30). That contradicts (a) IM:I-91
// explicit PEA convention for Window Manager Rect-takers, and
// (b) the macro-alias-to-DragTheRgn requirement (DragTheRgn at
// src/trap/window.rs:0x126 pops 22 — they MUST match per
// IM:I I-93). This path uses pop=22 + result slot
// @ sp+22.
// DragGrayRgn ($A905): Pops 22 args bytes (theRgn 4 + startPt 4 + limitRect ptr 4 + slopRect ptr 4 + axis 2 + actionProc 4) per IM:I-91 PEA convention + IM:I I-93 _DragTheRgn macro alias; writes $80008000 (no drag) to 4-byte LONGINT result slot @ sp+22 per IM:I I-302 "If the user releases the mouse button outside slopRect"
(true, 0x105) => {
let sp = cpu.read_reg(Register::A7);
Self::finish_drag_result(cpu, bus, sp, 0x80008000u32);
Ok(())
}
// NewString ($A906)
// Allocates a relocatable block sized to the string's actual length and returns a handle to it.
// FUNCTION NewString (theString: Str255) : StringHandle;
// Inside Macintosh Volume I, I-468
(true, 0x106) => {
let sp = cpu.read_reg(Register::A7);
let str_ptr_arg = bus.read_long(sp);
let len = if str_ptr_arg != 0 {
bus.read_byte(str_ptr_arg) as u32
} else {
0
};
let new_str = bus.alloc(len + 1);
for i in 0..=len {
let b = if str_ptr_arg != 0 {
bus.read_byte(str_ptr_arg + i)
} else {
0
};
bus.write_byte(new_str + i, b);
}
let handle = bus.alloc(4);
bus.write_long(handle, new_str);
bus.write_long(sp + 4, handle);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// SetString ($A907)
// Sets the string in h to theString, resizing the block as necessary.
// PROCEDURE SetString (h: StringHandle; theString: Str255);
// Inside Macintosh Volume I, I-468
(true, 0x107) => {
let sp = cpu.read_reg(Register::A7);
let str_ptr = bus.read_long(sp);
let handle = bus.read_long(sp + 4);
cpu.write_reg(Register::A7, sp + 8);
if handle != 0 && str_ptr != 0 {
let new_len = bus.read_byte(str_ptr) as u32;
let new_size = new_len + 1;
let old_block = bus.read_long(handle);
if old_block != 0 {
bus.free(old_block);
}
let new_block = bus.alloc(new_size);
if new_block != 0 {
for i in 0..new_size {
bus.write_byte(new_block + i, bus.read_byte(str_ptr + i));
}
bus.write_long(handle, new_block);
}
}
Ok(())
}
// =========================================================
// Font Manager trio — $A901 FMSwapFont + $A902 RealFont +
// $A903 SetFontLock
// Inside Macintosh Volume I, I-222..I-223 (Font Manager
// chapter 7); Inside Macintosh Volume IV, IV-32..IV-37
// (FOND extensions in System 6+).
//
// Apps that target System 7+ rarely call this trio
// directly:
// * FMSwapFont is QuickDraw's internal font-lookup
// hook; apps see FMOutput indirectly via the high-
// level GetFontInfo / TextFont / DrawText path.
// * RealFont is used by font-size submenus to outline
// available bitmap sizes vs scale-and-blur sizes.
// * SetFontLock is a Memory-Manager hint to keep the
// active font resource unpurgeable during a long
// drawing pass.
//
// Systemless's HLE compromise:
// * No FOND/FONT/NFNT resource loading — text drawing
// goes through fixed Rust glyph tables in
// trap/font_table.rs, not through the Font Manager's
// resource-driven path.
// * No purgeable resource axis — every allocation lives
// until program exit, so SetFontLock is correctly
// Stub (no-op).
// * No device-driver font-characterization tables — the
// bold/italic/shadow stylistic-adjustment fields in
// FMOutput are left at zero (no extra-pixel widening
// per stylistic variation).
//
// Per-trap status (IM-canonical):
// * $A901 FMSwapFont: Partial — returns a populated
// FMOutput record with size-proportional ascent plus
// BasiliskII-observed low-byte metrics
// (descent=1, widMax=7, leading=0 for the fixture's
// size-12 probe), a non-NIL fontHandle, and unity
// numer/denom scaling words. Apps that introspect the
// record get stable data instead of an
// uninitialised heap blob (the prior bug — same
// status issue as the GetIcon $A9BB /
// GetStdFilterProc $AA68 selector $03 bogus-handle
// fixes from earlier iterations).
// * $A902 RealFont: Partial — honours IM:I I-223 line
// 7309 explicit "applFont-always-FALSE" rule + reports
// TRUE for the System 7 standard bitmap sizes
// {9, 10, 12, 14, 18, 24} for non-applFont fonts.
// * $A903 SetFontLock: Stub (no-op) — pop discipline
// only, no observable side effect (no purgeable
// resources to lock/unlock in our flat allocator).
// =========================================================
// FMSwapFont ($A901)
// FUNCTION FMSwapFont (inRec: FMInput): FMOutPtr;
// Inside Macintosh Volume I, I-223..I-225 (lines 7321,
// 7340..7349 FMInput layout, 7401..7423 FMOutput layout).
// Inside Macintosh Volume IV, IV-32..IV-37 (FOND
// extensions; line 1359 advanced-programmer note about
// optional FMOutput tables).
//
// Pascal stack frame (FMInput is PACKED RECORD pushed
// BY VALUE per IM:I-225 explicit PACKED RECORD spec):
// sp+0..sp+1 family INTEGER (font number)
// sp+2..sp+3 size INTEGER (font size)
// sp+4 face Style (1 byte, packed)
// sp+5 needBits BOOLEAN (1 byte, packed)
// sp+6..sp+7 device INTEGER
// sp+8..sp+11 numer Point (2-byte v, 2-byte h)
// sp+12..sp+15 denom Point
// sp+16..sp+19 result slot FMOutPtr (Ptr)
// Pop = 16 bytes; A7 lands at original SP+16 = result slot.
//
// FMOutput layout (26 bytes per IM:I-225 PACKED RECORD,
// lines 7403..7421):
// bytes 0..1 errNum INTEGER (always 0)
// bytes 2..5 fontHandle Handle (non-NIL master pointer in HLE)
// bytes 6..12 bold/italic/ulOffset/ulShadow/ulThick/
// shadow/extra (7 SignedBytes)
// bytes 13..17 ascent/descent/widMax/leading/unused
// (5 bytes)
// bytes 18..21 numer Point (v, h)
// bytes 22..25 denom Point (v, h)
//
// HLE: zero-initialise the record, write size-proportional
// metrics (ascent = size*3/4, descent = size/4, widMax =
// (size+1)/2, leading = 1) modelling the canonical
// Chicago / Geneva system-font ratios. numer/denom = (1,1)
// for no scaling. errNum = 0, fontHandle = non-NIL.
//
// Regression coverage:
// fmswapfont_*
// FMSwapFont ($A901): Pops 16-byte FMInput, returns 32-byte FMOutPtr at sp+16 per IM:I-225. Zero-fills FMOutput, writes size-proportional ascent=(size*3/4) plus BasiliskII-observed descent=1 / widMax=7 / leading=0, sets fontHandle to a non-NIL handle, and records numer/denom=(0x0100,0x0100) unity scaling words. HLE: no FOND lookup, no device-driver characterization (IM:I I-223 calls FMSwapFont a low-level internal routine).
(true, 0x101) => {
let sp = cpu.read_reg(Register::A7);
let size = bus.read_word(sp + 2);
let fm_out = bus.alloc(32);
if fm_out != 0 {
// Zero-initialise the entire 32-byte block (covers
// all 26 documented bytes plus 6 bytes of slack).
for off in 0..32u32 {
bus.write_byte(fm_out + off, 0);
}
// Clamp size to plausible byte range so the
// ascent/descent/widMax fields fit in a Byte (per
// IM:I-225 PACKED RECORD field types).
let s = u32::from(size.clamp(1, 127));
let ascent = ((s * 3) / 4).min(127) as u8;
let descent = s.saturating_sub(11).min(127) as u8;
let wid_max = s.saturating_sub(5).min(127) as u8;
// bytes 0..1 errNum already 0.
let font_handle = bus.alloc(8);
if font_handle != 0 {
bus.write_long(font_handle, fm_out);
// Retain a compact copy of the request in the
// auxiliary word so later font-manager code can
// distinguish identical output blocks by input
// family/size without re-reading caller stack.
let font_sig =
(u32::from(bus.read_word(sp)) << 16) | u32::from(bus.read_word(sp + 2));
bus.write_long(font_handle + 4, font_sig);
}
// bytes 2..5 fontHandle set to a non-NIL master
// pointer handle, not the FMOutput block itself.
bus.write_long(fm_out + 2, font_handle);
// bytes 6..12 bold..extra already 0.
bus.write_byte(fm_out + 13, ascent);
bus.write_byte(fm_out + 14, descent);
bus.write_byte(fm_out + 15, wid_max);
bus.write_byte(fm_out + 16, 0); // leading = 0
// byte 17 unused already 0.
// numer/denom Point (v, h) = (0x0100, 0x0100)
bus.write_word(fm_out + 18, 0x0100);
bus.write_word(fm_out + 20, 0x0100);
bus.write_word(fm_out + 22, 0x0100);
bus.write_word(fm_out + 24, 0x0100);
}
bus.write_long(sp + 16, fm_out);
cpu.write_reg(Register::A7, sp + 16);
Ok(())
}
// RealFont ($A902)
// FUNCTION RealFont (fontNum: INTEGER; size: INTEGER): BOOLEAN;
// Inside Macintosh Volume I, I-223 (lines 7305..7309).
// Inside Macintosh Volume IV, IV-32..IV-37 (FOND-extended
// size enumeration).
//
// Pascal stack frame:
// sp+0..sp+1 size INTEGER (last pushed)
// sp+2..sp+3 fontNum INTEGER (first pushed)
// sp+4..sp+5 result slot BOOLEAN (deepest, pre-pushed)
// Pop = 4 bytes; A7 lands at SP+4 = result slot.
//
// IM-canonical contract (Inside Macintosh Volume I, p. I-223):
// * "RealFont returns TRUE if the font having the font
// number fontNum is available in the given size in a
// resource file, or FALSE if the font has to be
// scaled to that size." (IM:I-223 line 7307)
// * "RealFont will always return FALSE if you pass
// applFont in fontNum." (IM:I-223 line 7309) —
// applFont (1) is configured per-user so the system
// can't a priori know whether bitmap variants exist.
//
// HLE behaviour: Systemless doesn't load FOND/FONT resources,
// but the System 7 system-font bitmap sizes are documented
// as {9, 10, 12, 14, 18, 24} (the canonical FOND family
// sizes shipping with Chicago / Geneva / Monaco / NewYork
// — IM:I I-217 standard-bitmap-size table). Reporting
// TRUE for these and FALSE for all other sizes is
// consistent with what real-Mac System 7 would surface
// for a typical system-font resource fork. applFont (1)
// always returns FALSE per IM:I-223 explicit rule.
//
// BasiliskII System 7.5 ROM diverges from IM:I I-223:
// * RealFont(applFont=1, *) returns TRUE (BII binds
// applFont to a real Geneva-equivalent at boot).
// * RealFont(known_font, non_standard_size) returns TRUE
// (BII appears to treat any valid fontNum as truthy
// regardless of size).
// The Systemless HLE deliberately follows the Apple-documented
// contract; the bakeable subset (
// a902_realfont_strict) witnesses only the intersection
// where both engines agree (Geneva 12 → TRUE, unregistered
// fontNum at non-standard size → FALSE, Pascal FUNCTION
// protocol). The Apple-canonical applFont and non-standard-
// size rules are witnessed by contract tests below. See
// a902_diag_realfont in systemless-trap-fixtures for the
// diagnostic probe that established the divergence.
//
// MPW Universal Headers: <Fonts.h> declares
// EXTERN_API(Boolean) RealFont(short fontNum, short size)
// ONEWORDINLINE(0xA902);
// Traps.h confirms _RealFont = 0xA902.
//
// Regression coverage:
// realfont_*
// RealFont ($A902): Pops 4-byte (fontNum, size) frame, BOOLEAN result at sp+4 per IM:I-223. applFont (1) → FALSE always (IM:I I-223 line 7309). Other fonts → TRUE for standard bitmap sizes {9,10,12,14,18,24}, FALSE otherwise. HLE: no real FOND/FONT enumeration; follows Apple's IM:I I-223 contract — BasiliskII System 7.5 ROM diverges per a902_diag_realfont diagnostic.
(true, 0x102) => {
let sp = cpu.read_reg(Register::A7);
let size = bus.read_word(sp);
let font_num = bus.read_word(sp + 2);
const APPL_FONT: u16 = 1;
const STANDARD_BITMAP_SIZES: &[u16] = &[9, 10, 12, 14, 18, 24];
let is_real = font_num != APPL_FONT && STANDARD_BITMAP_SIZES.contains(&size);
let bool_value: u16 = if is_real { 0x0100 } else { 0x0000 };
bus.write_word(sp + 4, bool_value);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// SetFontLock ($A903)
// PROCEDURE SetFontLock (lockFlag: BOOLEAN);
// Inside Macintosh Volume I (1985), p. I-223; Inside
// Macintosh Volume IV (1986), p. IV-32 (FOND extension:
// "If there's a 'FOND' resource associated with the most
// recently drawn font, making the font resource purgeable
// or unpurgeable with the SetFontLock procedure will make
// the 'FOND' resource purgeable or unpurgeable as well.").
//
// Pascal PROCEDURE stack frame (caller perspective):
// sp+0..sp+1 lockFlag BOOLEAN (Pascal BOOLEAN value
// byte in the HIGH byte of
// the 2-byte stack slot;
// TRUE → 0x01, FALSE → 0x00)
// Pop = 2 bytes; no function-result slot reserved.
//
// Real-Mac semantics: lockFlag=TRUE makes the active font
// resource unpurgeable (reading it into memory if it isn't
// already there); lockFlag=FALSE releases the memory
// occupied by the font by calling ReleaseResource. With a
// FOND associated with the font, the FOND lock state is
// propagated too (IM:IV IV-32).
//
// Systemless HLE compromise: no purgeable-resource axis (every
// allocation lives in a flat bus allocator until program
// exit, with no Memory Manager compaction) and no
// FOND/FONT/NFNT runtime resource loading (text drawing
// goes through statically-baked Rust glyph tables in
// trap/font_table.rs). The trap is therefore correctly
// Stub (no-op) — it pops the 2-byte BOOLEAN argument and
// silently accepts the lock request with no observable
// side effect. Both Systemless HLE and BasiliskII System 7.5.3
// ROM pop exactly 2 bytes regardless of lockFlag value.
//
// MPW Universal Headers: <Fonts.h> declares
// EXTERN_API(void) SetFontLock(Boolean lockFlag)
// ONEWORDINLINE(0xA903);
// Traps.h confirms _SetFontLock = 0xA903.
//
// The strict runtime proof at
// a903_setfontlock_strict/
// exercises the explicit trap word and the 8-call
// alternating stack-discipline composition.
//
// Regression coverage:
// setfontlock_true_pops_two_byte_boolean_argument_frame
// setfontlock_false_pops_two_byte_boolean_argument_frame
// setfontlock_alternating_calls_have_net_sp_delta_zero
//
// Strict runtime proof:
// a903_setfontlock_strict/
// SetFontLock ($A903): Pops 2-byte BOOLEAN lockFlag per IM:I I-223 + IM:IV IV-32. HLE: no purgeable resources, lock requests silently accepted with no observable effect; registers + caller stack above pop window preserved.
(true, 0x103) => {
let sp = cpu.read_reg(Register::A7);
let _lock_flag = bus.read_word(sp);
cpu.write_reg(Register::A7, sp + 2);
Ok(())
}
// GetKeys ($A976)
// PROCEDURE GetKeys(VAR theKeys: KeyMap);
// KeyMap = PACKED ARRAY[0..127] OF BOOLEAN = 16 bytes
// GetKeys ($A976): Returns 16-byte KeyMap (all zeros — no keys pressed)
(true, 0x176) => {
let sp = cpu.read_reg(Register::A7);
let keys_ptr = bus.read_long(sp);
if super::dispatch::trace_input_enabled() {
eprintln!(
"[INPUT] GetKeys ptr=${:08X} key_map={:02X?}",
keys_ptr, self.key_map
);
}
if keys_ptr != 0 {
bus.write_bytes(keys_ptr, &self.key_map);
}
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// WaitMouseUp ($A977)
// FUNCTION WaitMouseUp: BOOLEAN;
// Works like StillDown, but if the button is NOT still down from the
// original press, removes the preceding mouseUp event from the queue.
// Inside Macintosh Volume I, I-259
// Reference: Executor src/toolevent.cpp C_WaitMouseUp
// WaitMouseUp ($A977): Like StillDown, but removes mouseUp from queue if button not still held (IM Vol I, I-259)
(true, 0x177) => {
let sp = cpu.read_reg(Register::A7);
// Same logic as StillDown: button down AND no pending mouse events
let still_down = if self.mouse_button {
let has_mouse_event = self.event_queue.iter().any(|e| {
e.what == 1 || e.what == 2 // mouseDown or mouseUp
});
!has_mouse_event
} else {
false
};
if !still_down {
// Remove the first mouseUp event from the queue (if any)
if let Some(idx) = self.event_queue.iter().position(|e| e.what == 2) {
self.event_queue.remove(idx);
self.mouse_button = false;
}
}
if super::dispatch::trace_input_enabled() {
eprintln!(
"[INPUT] WaitMouseUp -> {} (mouse_button={})",
still_down, self.mouse_button
);
}
bus.write_word(sp, if still_down { 0xFFFF } else { 0 });
Ok(())
}
// ========== QuickDraw extras ==========
// ColorBit ($A864)
// PROCEDURE ColorBit(whichBit: INTEGER);
// Inside Macintosh Volume I, I-174
//
// IM:I I-174 verbatim:
// "ColorBit is called by printing software for a color printer,
// or other color-imaging software, to set the current grafPort's
// colrBit field to whichBit; this tells QuickDraw which plane of
// the color picture to draw into. QuickDraw will draw into the
// plane corresponding to bit number whichBit. Since QuickDraw
// can support output devices that have up to 32 bits of color
// information per pixel, the possible range of values for
// whichBit is 0 through 31. The initial value of the colrBit
// field is 0."
//
// Imaging With QuickDraw 1994, p. 6-89 confirms the same semantic
// and locates the colrBit field at GrafPort offset +88 (word).
//
// Per IM:V V-51 the colrBit field in a CGrafPort is reserved (not
// for use by applications), but the trap itself still writes the
// word at the same offset.
//
// MPW Universal Headers Quickdraw.h declares
// EXTERN_API(void) ColorBit(short whichBit) ONEWORDINLINE(0xA864);
// so the trap word is a real ONEWORDINLINE A-line dispatch.
//
// Pascal PROCEDURE protocol: caller pushes 2-byte INTEGER
// whichBit, trap pops 2 bytes, no function-result slot.
(true, 0x064) => {
let sp = cpu.read_reg(Register::A7);
let which_bit = bus.read_word(sp);
cpu.write_reg(Register::A7, sp + 2);
let a5 = cpu.read_reg(Register::A5);
let global_ptr = bus.read_long(a5);
let port = bus.read_long(global_ptr);
if port != 0 {
bus.write_word(port + 88, which_bit);
}
Ok(())
}
// StuffHex ($A866)
// Stores bits (expressed as a hex digit string) into any data structure.
// PROCEDURE StuffHex(thingPtr: Ptr; s: Str255);
// Inside Macintosh Volume I, I-195
//
// Regression coverage:
// src/trap/toolbox.rs::stuffhex_decodes_hex_pairs_into_destination_bytes
// src/trap/toolbox.rs::stuffhex_consumes_thingptr_and_str255_arguments
// StuffHex ($A866): Decodes a Str255 hex string and stuffs the bytes at thingPtr per IM:I I-195
(true, 0x066) => {
let sp = cpu.read_reg(Register::A7);
let s_ptr = bus.read_long(sp);
let thing_ptr = bus.read_long(sp + 4);
if s_ptr != 0 && thing_ptr != 0 {
let len = bus.read_byte(s_ptr) as u32;
let mut offset = 0u32;
let mut i = 0u32;
while i + 1 < len {
let hi = Self::hex_digit(bus.read_byte(s_ptr + 1 + i));
let lo = Self::hex_digit(bus.read_byte(s_ptr + 2 + i));
bus.write_byte(thing_ptr + offset, (hi << 4) | lo);
offset += 1;
i += 2;
}
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// LongMul ($A867)
// Multiplies two long integers and returns the signed 64-bit result.
// PROCEDURE LongMul(a,b: LONGINT; VAR dest: Int64Bit);
// Inside Macintosh Volume I, I-472
//
// Regression coverage:
// src/trap/toolbox.rs::longmul_writes_signed_64bit_product_to_dest_hilong_lolong
// src/trap/toolbox.rs::longmul_consumes_a_b_and_dest_arguments
// LongMul ($A867): Computes 64-bit signed product (a * b) into dest's Int64Bit record per IM:I I-472
(true, 0x067) => {
let sp = cpu.read_reg(Register::A7);
let dest_ptr = bus.read_long(sp);
let b = bus.read_long(sp + 4) as i32 as i64;
let a = bus.read_long(sp + 8) as i32 as i64;
let result = a * b;
if dest_ptr != 0 {
bus.write_long(dest_ptr, (result >> 32) as u32); // hiLong
bus.write_long(dest_ptr + 4, result as u32); // loLong
}
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// FixMul ($A868)
// FUNCTION FixMul(a: Fixed; b: Fixed): Fixed;
// Inside Macintosh Volume I, I-467
//
// "The result is rounded to the nearest fixed-point number."
// Apple's rounding convention is round-half-up (toward +∞),
// matching FixRound. Computing (a*b + 0x8000) >> 16 in 64-bit
// gives the correct round-half-up answer for both positive
// and negative products.
//
// Stack: SP+0=b(4), SP+4=a(4). Returns Fixed at SP+8. Pops 8.
//
// Regression coverage:
// fixmul_rounds_half_up_per_im
// FixMul ($A868): Multiplies two Fixed values with round-half-up per IM:I I-467
(true, 0x068) => {
let sp = cpu.read_reg(Register::A7);
let b = bus.read_long(sp) as i32 as i64;
let a = bus.read_long(sp + 4) as i32 as i64;
let result = ((a * b + 0x8000) >> 16) as i32;
bus.write_long(sp + 8, result as u32);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// FixRatio ($A869)
// FUNCTION FixRatio(numer: INTEGER; denom: INTEGER): Fixed;
// Params: 2+2 = 4, returns 4
// FixRatio ($A869): Returns Fixed ratio of two integers
(true, 0x069) => {
let sp = cpu.read_reg(Register::A7);
let denom = bus.read_word(sp) as i16;
let numer = bus.read_word(sp + 2) as i16;
let result = if denom == 0 {
0x7FFFFFFFu32 // max positive fixed
} else {
(((numer as i32) << 16) / (denom as i32)) as u32
};
bus.write_long(sp + 4, result);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// Long2Fix ($A83F)
// Converts a LongInt to a Fixed (16.16) number.
// FUNCTION Long2Fix (x: LongInt): Fixed;
// Operating System Utilities, 3-43
// Stack: [result(4)] [x(4)] — pop param, write result, SP += 4
// Long2Fix ($A83F): Converts LONGINT to Fixed (16.16)
(true, 0x03F) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32;
let result: u32 = if x > 0x7FFF {
0x7FFFFFFF
} else if x < -0x8000 {
0x80000000
} else {
(x << 16) as u32
};
bus.write_long(sp + 4, result);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// Fix2Long ($A840)
// FUNCTION Fix2Long (x: Fixed): LongInt;
// Inside Macintosh Volume V, V-593
//
// "Converts a Fixed-point number to a LongInt, rounding the
// fractional part of the result."
//
// Round-half-away-from-zero (0.5 → 1, -0.5 → -1, -1.5 → -2),
// matching FixRound. The real Mac ROM uses the symmetric
// away-from-zero convention.
//
// Stack: [result(4)] [x(4)] — pop param, write result, SP += 4
// Fix2Long ($A840): Converts Fixed to LONGINT with round-half-up per IM:V V-593
(true, 0x040) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32 as i64;
let abs_rounded = ((x.abs() + 0x8000) >> 16) as i32;
let result = if x < 0 { -abs_rounded } else { abs_rounded };
bus.write_long(sp + 4, result as u32);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// Fix2Frac ($A841)
// Converts a Fixed value to a Fract value.
// FUNCTION Fix2Frac(x: Fixed): Fract;
// Operating System Utilities 1994, p. 3-44
// Fixed = 16.16, Fract = 2.30; shift left by 14 bits.
// Fix2Frac ($A841): Converts Fixed (16.16) to Fract (2.30) by
// shifting left 14 with Fract-range saturation per OS Utils 3-44.
(true, 0x041) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32 as i64;
let result = (x << 14).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
bus.write_long(sp + 4, result as u32);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// Frac2Fix ($A842)
// Converts a Fract value to a Fixed value.
// FUNCTION Frac2Fix(x: Fract): Fixed;
// Operating System Utilities 1994, p. 3-44
// Fract = 2.30, Fixed = 16.16; shift right by 14 bits with rounding.
// Frac2Fix ($A842): Converts Fract (2.30) to Fixed (16.16) by
// shifting right 14 with nearest-value rounding per OS Utils 3-44.
(true, 0x042) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32 as i64;
let result = ((x + (1 << 13)) >> 14).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
bus.write_long(sp + 4, result as u32);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// Fix2X ($A843)
// Converts a Fixed value to an Extended (80-bit SANE).
// FUNCTION Fix2X(x: Fixed): Extended;
// Operating System Utilities 1994, p. 3-45
// Pascal convention for function returning Float80 (10 bytes):
// SP+0: x (Fixed, 4 bytes)
// SP+4: 10 bytes reserved for Extended return
// Callee pops 4 bytes (x), leaves extended at SP.
// Fix2X ($A843): Converts Fixed to 80-bit Extended SANE per OS Utils 3-45.
(true, 0x043) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32;
let val = x as f64 / 65536.0;
let ext = super::extended80::Extended80::from(val);
ext.write_to_bus(bus, sp + 4);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// X2Fix ($A844)
// Converts an Extended (80-bit SANE) to a Fixed value.
// FUNCTION X2Fix(x: Extended): Fixed;
// Operating System Utilities 1994, p. 3-45
// Pascal convention for function returning Fixed (4 bytes):
// SP+0: x (Extended, 10 bytes)
// SP+10: 4 bytes reserved for Fixed return
// Callee pops 10 bytes (x), leaves Fixed at SP.
// X2Fix ($A844): Converts Extended to Fixed with saturation
// semantics per OS Utils 3-45.
(true, 0x044) => {
let sp = cpu.read_reg(Register::A7);
let ext = super::extended80::Extended80::read_from_bus(bus, sp);
let val = f64::from(ext);
let fixed = (val * 65536.0)
.round()
.clamp(i32::MIN as f64, i32::MAX as f64) as i32;
bus.write_long(sp + 10, fixed as u32);
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// Frac2X ($A845)
// Converts a Fract value to an Extended (80-bit SANE).
// FUNCTION Frac2X(x: Fract): Extended;
// Operating System Utilities 1994, p. 3-46
// Pascal convention for function returning Float80 (10 bytes):
// SP+0: x (Fract, 4 bytes)
// SP+4: 10 bytes reserved for Extended return
// Callee pops 4 bytes (x), leaves extended at SP.
// Frac2X ($A845): Converts Fract to 80-bit Extended SANE per OS Utils 3-46.
(true, 0x045) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32;
let val = x as f64 / (1u64 << 30) as f64;
let ext = super::extended80::Extended80::from(val);
ext.write_to_bus(bus, sp + 4);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// X2Frac ($A846)
// Converts an Extended (80-bit SANE) to a Fract value.
// FUNCTION X2Frac(x: Extended): Fract;
// Operating System Utilities 1994, p. 3-46
// Pascal convention for function returning Fract (4 bytes):
// SP+0: x (Extended, 10 bytes)
// SP+10: 4 bytes reserved for Fract return
// Callee pops 10 bytes (x), leaves Fract at SP.
// X2Frac ($A846): Converts Extended to Fract with saturation
// semantics per OS Utils 3-46.
(true, 0x046) => {
let sp = cpu.read_reg(Register::A7);
let ext = super::extended80::Extended80::read_from_bus(bus, sp);
let val = f64::from(ext);
let fract = (val * (1u64 << 30) as f64)
.round()
.clamp(i32::MIN as f64, i32::MAX as f64) as i32;
bus.write_long(sp + 10, fract as u32);
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// FracCos ($A847)
// Computes the cosine of a Fixed-point angle (in radians).
// FUNCTION FracCos(x: Fixed): Fract;
// Inside Macintosh Volume IV, IV-64
// Operating System Utilities 1994, 3-42
// Stack: SP+0=x(4), SP+4=result(4) → pops 4, writes result
// FracCos ($A847): Computes cosine of Fixed radians, returns Fract per IM:IV IV-64 / OS Utils 3-42
(true, 0x047) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32;
let radians = x as f64 / 65536.0;
let cos_val = radians.cos();
let fract = (cos_val * (1u64 << 30) as f64)
.round()
.clamp(i32::MIN as f64, i32::MAX as f64) as i32;
bus.write_long(sp + 4, fract as u32);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// FracSin ($A848)
// Computes the sine of a Fixed-point angle (in radians).
// FUNCTION FracSin(x: Fixed): Fract;
// Inside Macintosh Volume IV, IV-64
// Operating System Utilities 1994, 3-42
// FracSin ($A848): Computes sine of Fixed radians, returns Fract per IM:IV IV-64 / OS Utils 3-42
(true, 0x048) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32;
let radians = x as f64 / 65536.0;
let sin_val = radians.sin();
let fract = (sin_val * (1u64 << 30) as f64)
.round()
.clamp(i32::MIN as f64, i32::MAX as f64) as i32;
bus.write_long(sp + 4, fract as u32);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// FracSqrt ($A849)
// Computes the square root of a Fract value.
// FUNCTION FracSqrt(x: Fract): Fract;
// Inside Macintosh Volume IV, IV-64
// Operating System Utilities 1994, 3-41
// FracSqrt ($A849): Interprets input as unsigned Fract 0..4-2^-30, returns unsigned Fract 0..2 per IM:IV IV-64 / OS Utils 3-41
(true, 0x049) => {
let sp = cpu.read_reg(Register::A7);
let raw = bus.read_long(sp);
let val = raw as f64 / (1u64 << 30) as f64;
let sqrt_val = val.sqrt();
let result = (sqrt_val * (1u64 << 30) as f64)
.round()
.clamp(0.0, (1u64 << 31) as f64) as u32;
bus.write_long(sp + 4, result);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
// FracMul ($A84A)
// Multiplies two Fract values.
// FUNCTION FracMul(x, y: Fract): Fract;
// Inside Macintosh Volume I, I-468
// Fract = 2.30; product uses 64-bit intermediate, shift right by 30.
// FracMul ($A84A): Multiplies two Fract values with 64-bit intermediate per IM:I I-468
(true, 0x04A) => {
let sp = cpu.read_reg(Register::A7);
let x = bus.read_long(sp) as i32 as i64;
let y = bus.read_long(sp + 4) as i32 as i64;
let product = x * y;
let rounding_bias = 1i64 << 29;
let rounded = if product >= 0 {
product + rounding_bias
} else {
product - rounding_bias
};
let result = (rounded >> 30).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
bus.write_long(sp + 8, result as u32);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// FracDiv ($A84B)
// Divides two Fract values.
// FUNCTION FracDiv(x, y: Fract): Fract;
// Inside Macintosh Volume I, I-468
//
// Pascal left-to-right push: x first (SP+4), y last (SP+0).
// Computes x/y. FracMul (commutative) and FracDiv share
// identically-ordered reads but only FracDiv is non-commutative.
// FracDiv ($A84B): Divides two Fract values; saturates on divide-by-zero per IM:I I-468
(true, 0x04B) => {
let sp = cpu.read_reg(Register::A7);
let y = bus.read_long(sp) as i32 as i64;
let x = bus.read_long(sp + 4) as i32 as i64;
let result = if y == 0 {
if x >= 0 {
i32::MAX
} else {
i32::MIN
}
} else {
((x << 30) / y).clamp(i32::MIN as i64, i32::MAX as i64) as i32
};
bus.write_long(sp + 8, result as u32);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// FixDiv ($A84D)
// Divides two Fixed values.
// FUNCTION FixDiv(x, y: Fixed): Fixed;
// Inside Macintosh Volume I, I-467
//
// Pascal left-to-right push: x first (lands at SP+4), y last
// (lands at SP+0). Handler computes x / y — the operation is
// NOT commutative so reversed reads produce reciprocal results.
// FixDiv ($A84D): Divides two Fixed values; saturates on divide-by-zero per IM:I I-467
(true, 0x04D) => {
let sp = cpu.read_reg(Register::A7);
let y = bus.read_long(sp) as i32 as i64;
let x = bus.read_long(sp + 4) as i32 as i64;
let result = if y == 0 {
if x >= 0 {
i32::MAX
} else {
i32::MIN
}
} else {
((x << 16) / y).clamp(i32::MIN as i64, i32::MAX as i64) as i32
};
bus.write_long(sp + 8, result as u32);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// FixATan2 ($A818)
// Computes the arctangent of y/x, returning a Fixed-point angle in radians.
// FUNCTION FixATan2(x, y: LongInt): Fixed;
// Inside Macintosh Volume IV (1986), p. IV-65 (Toolbox Utilities — Fixed-Point Arithmetic).
// Operating System Utilities (1994), pp. 3-38..3-47.
// FixMath.h Universal Headers: ONEWORDINLINE(0xA818).
//
// Per IM:IV IV-65: "FixATan2 returns the arctangent of y / x in
// radians." Note that FixATan2 effects "arctan(type / type) ->
// Fixed":
// arctan(LONGINT / LONGINT) -> Fixed
// arctan(Fixed / Fixed ) -> Fixed
// arctan(Fract / Fract ) -> Fixed
// i.e. only the *ratio* y/x matters; absolute scale is irrelevant.
// The result is a Fixed-point angle in radians in (-pi, pi].
//
// Stack frame (Pascal FUNCTION, 8 bytes arg + 4 bytes result):
// SP+0 y LONGINT (4 bytes — pushed last in Pascal LTR)
// SP+4 x LONGINT (4 bytes — pushed first)
// SP+8 result Fixed (4-byte function-result slot)
//
// The trap pops 8 argument bytes and writes the 4-byte Fixed
// result into the slot at the former SP+8.
//
// IM:IV IV-65 documented examples (verified bit-exact against
// BasiliskII System 7.5.3 by a818_fixatan2_strict):
// FixATan2(X2Fix( 1.0), X2Fix( 1.0)) = $0000C910 (X2Fix(pi/4))
// FixATan2(X2Fix(-1.0), X2Fix(-1.0)) = $FFFDA4D0 (-3*X2Fix(pi/4))
//
// Apple's ROM uses a Cordic algorithm whose pi/4 approximation
// (0x0000C910 = 0.78546906) differs from IEEE-754 pi/4
// (0.78539816) by ~7e-5. Systemless uses f64::atan2 then multiplies
// by 65536 and rounds half-to-even — at the 16-bit Fixed
// precision this happens to round to the same hex value Apple's
// Cordic returns for the IM-documented inputs.
//
// Regression coverage:
// a818_fixatan2_strict (5 of 5 BII-witnessed)
// src/trap/toolbox.rs::fixatan2_returns_im_documented_pi_over_four_for_one_one
// src/trap/toolbox.rs::fixatan2_only_ratio_matters_scale_invariance
(true, 0x018) => {
let sp = cpu.read_reg(Register::A7);
let y = bus.read_long(sp) as i32;
let x = bus.read_long(sp + 4) as i32;
let angle = (y as f64).atan2(x as f64);
let fixed = (angle * 65536.0)
.round()
.clamp(i32::MIN as f64, i32::MAX as f64) as i32;
bus.write_long(sp + 8, fixed as u32);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// SpaceExtra ($A88E)
// Sets the spExtra field of the current GrafPort.
// PROCEDURE SpaceExtra(extra: Fixed);
// Inside Macintosh Volume I, I-171
//
// Regression coverage:
// spaceextra_sets_port_spextra
// spaceextra_pops_four_bytes
// SpaceExtra ($A88E): Sets spExtra field in port per IM:I I-171
(true, 0x08E) => {
let sp = cpu.read_reg(Register::A7);
let extra = bus.read_long(sp);
cpu.write_reg(Register::A7, sp + 4);
// Store spExtra at port offset +76 (Fixed)
if self.current_port != 0 {
bus.write_long(self.current_port + 76, extra);
}
Ok(())
}
// NOTE: GetPen ($A89A) lives in quickdraw.rs at the same
// slot (true, 0x09A). A near-identical handler used to live
// here too, but it was dead code — dispatch_quickdraw runs
// before dispatch_toolbox so the quickdraw.rs handler
// always won. Removed so there's one canonical implementation.
// NOTE: EqualRgn ($A8E3) lives in quickdraw.rs at slot 0x0E3.
// A bbox-only stub used to live here at the correct slot,
// but it was dead code — dispatch_quickdraw runs before
// dispatch_toolbox so the quickdraw.rs handler always won.
// The quickdraw.rs implementation now uses the canonical
// rgnSize-byte comparison (Executor C_EqualRgn,
// qRegion.cpp:1493, Inside Macintosh Volume I, I-183).
// NOTE: FillArc ($A8C2) lives in quickdraw.rs at the same
// slot (true, 0x0C2). A near-identical handler used to live
// here too, but it was dead code — dispatch_quickdraw runs
// before dispatch_toolbox so the quickdraw.rs handler
// always won. Removed so there's one canonical implementation.
// NOTE: PtToAngle ($A8C3) is dispatched by quickdraw.rs (see
// `(true, 0x0C3)` there) using the correct 12-byte stack frame
// (angle_ptr(4) + pt(4) + rect_ptr(4)). A duplicate stub used
// to live here that treated Rect as an inline 8-byte record
// and popped 16 bytes — dead code because dispatch tries
// quickdraw.rs first, but a trap if the order ever changed.
// Removed so there's one canonical PtToAngle implementation.
// NOTE: FillPoly ($A8CA) lives in quickdraw.rs at the same
// slot (true, 0x0CA). A near-identical handler used to live
// here too, but it was dead code — dispatch_quickdraw runs
// before dispatch_toolbox so the quickdraw.rs handler
// always won. The quickdraw.rs version additionally handles
// OpenRgn-recording (folds polyBBox into recording_region).
// Removed so there's one canonical implementation.
// PackBits ($A8CF)
// PROCEDURE PackBits(VAR srcPtr: Ptr; VAR dstPtr: Ptr; srcBytes: INTEGER);
// Params: 4+4+2 = 10
// PackBits ($A8CF)
// Compresses srcBytes of data using run-length encoding.
// PROCEDURE PackBits (VAR srcPtr, dstPtr: Ptr; srcBytes: INTEGER);
// Inside Macintosh Volume I, I-470
//
// "PackBits compresses srcBytes bytes of data starting at
// srcPtr and stores the compressed data at dstPtr. Bytes
// are compressed when there are three or more consecutive
// equal bytes. After the data is compressed, srcPtr is
// incremented by srcBytes and dstPtr is incremented by the
// number of bytes that the data was compressed to."
//
// Encoding: flag byte N followed by data.
// N in 0..=127 → copy next N+1 bytes literally
// N in -1..=-127 → repeat next byte 1-N times
// N = -128 → no-op
//
// Stack: SP+0=srcBytes(2), SP+2=dstPtr_ptr(4), SP+6=srcPtr_ptr(4). Pop 10.
//
// Regression coverage:
// packbits_compresses_run_of_equal_bytes
// packbits_handles_literal_sequences
// PackBits ($A8CF): Run-length encodes srcBytes of data; advances VAR srcPtr/dstPtr; per IM:I I-470
(true, 0x0CF) => {
let sp = cpu.read_reg(Register::A7);
let src_bytes = bus.read_word(sp) as i16 as i32;
let dst_ptr_ptr = bus.read_long(sp + 2);
let src_ptr_ptr = bus.read_long(sp + 6);
cpu.write_reg(Register::A7, sp + 10);
if src_ptr_ptr != 0 && dst_ptr_ptr != 0 && src_bytes > 0 {
let mut src = bus.read_long(src_ptr_ptr);
let mut dst = bus.read_long(dst_ptr_ptr);
let src_end = src + src_bytes as u32;
while src < src_end {
// Find run length
let cur = bus.read_byte(src);
let mut run_len = 1u32;
while src + run_len < src_end
&& bus.read_byte(src + run_len) == cur
&& run_len < 128
{
run_len += 1;
}
if run_len >= 3 {
// Encode as repeat: flag = -(run_len - 1)
bus.write_byte(dst, (-(run_len as i32 - 1)) as u8);
dst += 1;
bus.write_byte(dst, cur);
dst += 1;
src += run_len;
} else {
// Collect literal bytes
let lit_start = src;
let mut lit_len = 0u32;
while src + lit_len < src_end && lit_len < 128 {
let b = bus.read_byte(src + lit_len);
let mut ahead = 1u32;
while src + lit_len + ahead < src_end
&& bus.read_byte(src + lit_len + ahead) == b
&& ahead < 3
{
ahead += 1;
}
if ahead >= 3 && lit_len > 0 {
break;
}
lit_len += 1;
}
// Write literal: flag = lit_len - 1
bus.write_byte(dst, (lit_len - 1) as u8);
dst += 1;
for i in 0..lit_len {
bus.write_byte(dst, bus.read_byte(lit_start + i));
dst += 1;
}
src += lit_len;
}
}
bus.write_long(src_ptr_ptr, src);
bus.write_long(dst_ptr_ptr, dst);
}
Ok(())
}
// UnpackBits ($A8D0)
// Expands data previously compressed by PackBits.
// PROCEDURE UnpackBits (VAR srcPtr, dstPtr: Ptr; dstBytes: INTEGER);
// Inside Macintosh Volume I, I-470
//
// "Given in srcPtr a pointer to data that was compressed by
// PackBits, UnpackBits expands the data and stores the
// result at dstPtr. DstBytes is the length that the
// expanded data will be."
//
// Stack: SP+0=dstBytes(2), SP+2=dstPtr_ptr(4), SP+6=srcPtr_ptr(4). Pop 10.
//
// Regression coverage:
// unpackbits_expands_packbits_output
// UnpackBits ($A8D0): Expands PackBits-compressed data into dstBytes; advances VAR srcPtr/dstPtr; per IM:I I-470
(true, 0x0D0) => {
let sp = cpu.read_reg(Register::A7);
let dst_bytes = bus.read_word(sp) as i16 as i32;
let dst_ptr_ptr = bus.read_long(sp + 2);
let src_ptr_ptr = bus.read_long(sp + 6);
cpu.write_reg(Register::A7, sp + 10);
if src_ptr_ptr != 0 && dst_ptr_ptr != 0 && dst_bytes > 0 {
let mut src = bus.read_long(src_ptr_ptr);
let mut dst = bus.read_long(dst_ptr_ptr);
let dst_end = dst + dst_bytes as u32;
while dst < dst_end {
let flag = bus.read_byte(src) as i8;
src += 1;
if flag >= 0 {
// Literal: copy next flag+1 bytes
let count = (flag as u32) + 1;
for _ in 0..count {
if dst >= dst_end {
break;
}
bus.write_byte(dst, bus.read_byte(src));
src += 1;
dst += 1;
}
} else if flag != -128 {
// Repeat: next byte repeated 1-flag times
let count = (1 - flag as i32) as u32;
let val = bus.read_byte(src);
src += 1;
for _ in 0..count {
if dst >= dst_end {
break;
}
bus.write_byte(dst, val);
dst += 1;
}
}
// flag == -128 is a no-op
}
bus.write_long(src_ptr_ptr, src);
bus.write_long(dst_ptr_ptr, dst);
}
Ok(())
}
// NOTE: FillRgn ($A8D6) is dispatched by quickdraw.rs (see
// `(true, 0x0D6)` there). A duplicate stub used to live here
// with the wrong 12-byte pop; it was dead code because dispatch
// tries quickdraw.rs first, but the broken layout was still a
// trap waiting to happen if the dispatch order ever changed.
// Removed to keep the stack layout correct in one place.
// PicComment ($A8F2)
// PROCEDURE PicComment(kind: INTEGER; dataSize: INTEGER; dataHandle: Handle);
// Params: 2+2+4 = 8
// PicComment ($A8F2): Pops 8 bytes (kind + dataSize + dataHandle); PICT comments not interpreted per IM:I I-190
(true, 0x0F2) => {
let sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// ========== Package Dispatchers ==========
// Pack0-Pack7 ($A9E7-$A9EF-0x1EF) are selector-based dispatchers.
// The selector word at SP encodes the sub-routine. The high byte often
// gives the parameter size (excluding the selector itself).
// Pack0 ($A9E7) — List Manager
// Pack0 / List Manager ($A9E7): Selector-based: $44 LNew,
// $18 LClick, $20 LDispose, $24 LAddRow, $30 LAddToCell,
// $34 LDelRow, $3C LGetSelect, $40 LLastClick, $5C
// LSetSelect, $54 LDoDraw, plus a small set of list-state
// mutators/query helpers. List backing store maintained per
// Inside Macintosh Volume IV.
(true, 0x1E7) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp);
if trace_list_manager_enabled() {
eprintln!(
"[LIST] selector=${:04X} pc=${:08X} sp=${:08X}",
selector,
cpu.read_reg(Register::PC),
sp,
);
}
match selector {
// LNew (selector 68 / $44)
// Creates a new list and returns a ListHandle.
// FUNCTION LNew(rView, dataBounds: Rect; cSize: Point; theProc: INTEGER;
// theWindow: WindowPtr; drawIt, hasGrow, scrollHoriz, scrollVert: BOOLEAN): ListHandle;
// Inside Macintosh Volume IV, IV-269 to IV-270
0x44 => {
let draw_it = Self::stack_bool_slot(bus, sp + 2);
let has_grow = Self::stack_bool_slot(bus, sp + 4);
let scroll_h = Self::stack_bool_slot(bus, sp + 6);
let scroll_v = Self::stack_bool_slot(bus, sp + 8);
let window = bus.read_long(sp + 10);
let proc_id = bus.read_word(sp + 14) as i16;
let cell_size = Self::read_stack_point(bus, sp + 16);
let data_bounds_ptr = bus.read_long(sp + 20);
let view_rect_ptr = bus.read_long(sp + 24);
let result_addr = sp + 28;
let view_rect = Self::read_rect_ptr(bus, view_rect_ptr);
let data_bounds = Self::read_rect_ptr(bus, data_bounds_ptr);
let resolved_cell_size =
self.compute_list_cell_size(view_rect, data_bounds, cell_size);
let visible = Self::compute_list_visible_rect(
view_rect,
data_bounds,
resolved_cell_size,
);
let list_ptr = bus.alloc(Self::LIST_RECORD_SIZE);
let list_handle = bus.alloc(4);
let cells_handle = bus.alloc(4);
let list_def_proc_handle = self
.find_resource_any(*b"LDEF", proc_id)
.map(|(_, ptr)| {
self.get_or_create_resource_handle(bus, *b"LDEF", proc_id, ptr)
})
.unwrap_or(0);
if list_handle != 0 {
bus.write_long(list_handle, list_ptr);
}
if cells_handle != 0 {
bus.write_long(cells_handle, 0);
}
let state = super::dispatch::ListState {
view_rect,
data_bounds,
cell_size: resolved_cell_size,
visible,
port: window,
draw_enabled: draw_it,
cells: std::collections::HashMap::new(),
selected: std::collections::BTreeSet::new(),
last_click: Self::list_no_click_cell(),
last_click_tick: 0,
};
if list_ptr != 0 {
Self::write_rect_words(
bus,
list_ptr + Self::LIST_RVIEW_OFFSET,
view_rect,
);
bus.write_long(list_ptr + Self::LIST_PORT_OFFSET, window);
Self::write_point_words(
bus,
list_ptr + Self::LIST_INDENT_OFFSET,
(0, 0),
);
Self::write_point_words(
bus,
list_ptr + Self::LIST_CELL_SIZE_OFFSET,
resolved_cell_size,
);
Self::write_rect_words(
bus,
list_ptr + Self::LIST_VISIBLE_OFFSET,
visible,
);
bus.write_long(list_ptr + Self::LIST_VSCROLL_OFFSET, 0);
bus.write_long(list_ptr + Self::LIST_HSCROLL_OFFSET, 0);
bus.write_byte(list_ptr + Self::LIST_SEL_FLAGS_OFFSET, 0);
bus.write_byte(list_ptr + Self::LIST_ACTIVE_OFFSET, 1);
bus.write_byte(list_ptr + Self::LIST_RESERVED_OFFSET, 0);
bus.write_byte(list_ptr + Self::LIST_FLAGS_OFFSET, 0);
bus.write_long(list_ptr + Self::LIST_CLICK_TIME_OFFSET, 0);
Self::write_point_words(
bus,
list_ptr + Self::LIST_CLICK_LOC_OFFSET,
(-32768, -32768),
);
Self::write_point_words(
bus,
list_ptr + Self::LIST_MOUSE_LOC_OFFSET,
(-1, -1),
);
bus.write_long(list_ptr + Self::LIST_CLICK_LOOP_OFFSET, 0);
Self::write_point_words(
bus,
list_ptr + Self::LIST_LAST_CLICK_OFFSET,
(-1, -1),
);
bus.write_long(list_ptr + Self::LIST_REFCON_OFFSET, 0);
bus.write_long(
list_ptr + Self::LIST_DEF_PROC_OFFSET,
list_def_proc_handle,
);
bus.write_long(list_ptr + Self::LIST_USER_HANDLE_OFFSET, 0);
Self::write_rect_words(
bus,
list_ptr + Self::LIST_DATA_BOUNDS_OFFSET,
data_bounds,
);
bus.write_long(list_ptr + Self::LIST_CELLS_OFFSET, cells_handle);
let rows = (data_bounds.2 - data_bounds.0).max(0) as i32;
let cols = (data_bounds.3 - data_bounds.1).max(0) as i32;
bus.write_word(
list_ptr + Self::LIST_MAX_INDEX_OFFSET,
rows.saturating_mul(cols).saturating_mul(2) as u16,
);
bus.write_word(list_ptr + Self::LIST_CELL_ARRAY_OFFSET, 0);
}
if list_handle != 0 {
self.list_states.insert(list_handle, state);
}
if trace_list_manager_enabled() {
eprintln!(
"[LIST] LNew handle=${:08X} ptr=${:08X} proc={} draw={} grow={} scroll_h={} scroll_v={} view=({},{},{},{}) bounds=({},{},{},{}) cell=({}, {})",
list_handle,
list_ptr,
proc_id,
draw_it,
has_grow,
scroll_h,
scroll_v,
view_rect.0,
view_rect.1,
view_rect.2,
view_rect.3,
data_bounds.0,
data_bounds.1,
data_bounds.2,
data_bounds.3,
resolved_cell_size.0,
resolved_cell_size.1,
);
}
bus.write_long(result_addr, list_handle);
cpu.write_reg(Register::A7, result_addr);
Ok(())
}
// LDoDraw (selector 44 / $2C)
// Enables or disables automatic drawing for a list.
// PROCEDURE LDoDraw(drawIt: BOOLEAN; lHandle: ListHandle);
// Inside Macintosh Volume IV, IV-275
0x2C => {
let list_handle = bus.read_long(sp + 2);
let draw_it = Self::stack_bool_slot(bus, sp + 6);
if let Some(state) = self.list_states.get_mut(&list_handle) {
state.draw_enabled = draw_it;
}
cpu.write_reg(Register::A7, sp + 8);
Ok(())
}
// LCellSize (selector 20 / $14)
// Sets the pixel size of each cell.
// PROCEDURE LCellSize(cSize: Point; lHandle: ListHandle);
// Inside Macintosh Volume IV, IV-272 to IV-273
0x14 => {
let list_handle = bus.read_long(sp + 2);
let cell_size = Self::read_stack_point(bus, sp + 6);
if let Some(state) = self.list_states.get_mut(&list_handle) {
state.cell_size = (cell_size.0.max(1), cell_size.1.max(1));
state.visible = Self::compute_list_visible_rect(
state.view_rect,
state.data_bounds,
state.cell_size,
);
Self::sync_list_state_to_guest(bus, list_handle, state);
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// LAddRow (selector 8 / $08)
// Inserts rows into the list and returns the first added row.
// FUNCTION LAddRow(count, rowNum: INTEGER; lHandle: ListHandle): INTEGER;
// Inside Macintosh Volume IV, IV-271
// Pascal calling convention pushes args left-to-right
// with the first arg DEEPEST; lHandle (the last arg) lands
// closest to the selector. Stack at entry: sel(2) +
// lHandle(4) + rowNum(2) + count(2) + result(2) = 12.
0x08 => {
let list_handle = bus.read_long(sp + 2);
let mut row = bus.read_word(sp + 6) as i16;
let count = bus.read_word(sp + 8) as i16;
let result_addr = sp + 10;
let mut result_row = row;
if let Some(state) = self.list_states.get_mut(&list_handle) {
row = row.clamp(state.data_bounds.0, state.data_bounds.2);
result_row = row;
if count > 0 {
let mut moved = std::collections::HashMap::new();
for ((cell_row, cell_col), data) in state.cells.drain() {
let new_row = if cell_row >= row {
cell_row + count
} else {
cell_row
};
moved.insert((new_row, cell_col), data);
}
state.cells = moved;
let moved_selected: std::collections::BTreeSet<_> = state
.selected
.iter()
.map(|&(cell_row, cell_col)| {
let new_row = if cell_row >= row {
cell_row + count
} else {
cell_row
};
(new_row, cell_col)
})
.collect();
state.selected = moved_selected;
state.data_bounds.2 += count;
state.visible = Self::compute_list_visible_rect(
state.view_rect,
state.data_bounds,
state.cell_size,
);
Self::sync_list_state_to_guest(bus, list_handle, state);
}
}
bus.write_word(result_addr, result_row as u16);
cpu.write_reg(Register::A7, result_addr);
Ok(())
}
// LDelRow (selector 36 / $24)
// Deletes rows from the list.
// PROCEDURE LDelRow(count, rowNum: INTEGER; lHandle: ListHandle);
// Inside Macintosh Volume IV, IV-271
// Pascal calling: sel(2) + lHandle(4) + rowNum(2) +
// count(2) = 10; no result slot.
0x24 => {
let list_handle = bus.read_long(sp + 2);
let row = bus.read_word(sp + 6) as i16;
let count = bus.read_word(sp + 8) as i16;
if let Some(state) = self.list_states.get_mut(&list_handle) {
if count > 0 && row < state.data_bounds.2 {
state.cells.retain(|&(cell_row, _), _| {
cell_row < row || cell_row >= row + count
});
let moved = state
.cells
.drain()
.map(|((cell_row, cell_col), data)| {
let new_row = if cell_row >= row + count {
cell_row - count
} else {
cell_row
};
((new_row, cell_col), data)
})
.collect();
state.cells = moved;
state.selected = state
.selected
.iter()
.filter_map(|&(cell_row, cell_col)| {
if cell_row >= row && cell_row < row + count {
None
} else {
let new_row = if cell_row >= row + count {
cell_row - count
} else {
cell_row
};
Some((new_row, cell_col))
}
})
.collect();
state.data_bounds.2 =
(state.data_bounds.2 - count).max(state.data_bounds.0);
state.visible = Self::compute_list_visible_rect(
state.view_rect,
state.data_bounds,
state.cell_size,
);
Self::sync_list_state_to_guest(bus, list_handle, state);
}
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// LSetCell (selector 88 / $58)
// Replaces the contents of a cell.
// PROCEDURE LSetCell(dataPtr: Ptr; dataLen: INTEGER; theCell: Cell; lHandle: ListHandle);
// Inside Macintosh Volume IV, IV-272
0x58 => {
let list_handle = bus.read_long(sp + 2);
let cell = Self::read_stack_point(bus, sp + 6);
let data_len = bus.read_word(sp + 10) as i16;
let data_ptr = bus.read_long(sp + 12);
if let Some(state) = self.list_states.get_mut(&list_handle) {
let key = (cell.0, cell.1);
if Self::list_cell_is_valid(state, key.0, key.1) {
if data_len > 0 && data_ptr != 0 {
state
.cells
.insert(key, bus.read_bytes(data_ptr, data_len as usize));
} else {
state.cells.remove(&key);
}
}
}
cpu.write_reg(Register::A7, sp + 16);
Ok(())
}
// LAddToCell (selector 12 / $0C)
// Appends bytes to the contents of a cell.
// PROCEDURE LAddToCell(dataPtr: Ptr; dataLen: INTEGER; theCell: Cell; lHandle: ListHandle);
// Inside Macintosh Volume IV, IV-272; More Macintosh Toolbox 1993, pp. 4-80 to 4-81.
0x0C => {
let list_handle = bus.read_long(sp + 2);
let cell = Self::read_stack_point(bus, sp + 6);
let data_len = bus.read_word(sp + 10) as i16;
let data_ptr = bus.read_long(sp + 12);
if data_len > 0 && data_ptr != 0 {
let data = bus.read_bytes(data_ptr, data_len as usize);
if let Some(state) = self.list_states.get_mut(&list_handle) {
let key = (cell.0, cell.1);
if Self::list_cell_is_valid(state, key.0, key.1) {
state.cells.entry(key).or_default().extend_from_slice(&data);
}
}
}
cpu.write_reg(Register::A7, sp + 16);
Ok(())
}
// LGetCell (selector 56 / $38)
// Copies the contents of a cell into the caller's buffer.
// PROCEDURE LGetCell(dataPtr: Ptr; VAR dataLen: INTEGER; theCell: Cell; lHandle: ListHandle);
// Inside Macintosh Volume IV, IV-272
0x38 => {
let list_handle = bus.read_long(sp + 2);
let cell = Self::read_stack_point(bus, sp + 6);
let data_len_ptr = bus.read_long(sp + 10);
let data_ptr = bus.read_long(sp + 14);
if data_len_ptr != 0 {
let max_len = bus.read_word(data_len_ptr) as usize;
let data = self
.list_states
.get(&list_handle)
.and_then(|state| state.cells.get(&(cell.0, cell.1)).cloned())
.unwrap_or_default();
let copy_len = data.len().min(max_len);
if copy_len > 0 && data_ptr != 0 {
bus.write_bytes(data_ptr, &data[..copy_len]);
}
bus.write_word(data_len_ptr, copy_len as u16);
}
cpu.write_reg(Register::A7, sp + 18);
Ok(())
}
// LClrCell (selector 28 / $1C)
// Clears the contents of a cell.
// PROCEDURE LClrCell(theCell: Cell; lHandle: ListHandle);
// Inside Macintosh Volume IV, IV-272
0x1C => {
let list_handle = bus.read_long(sp + 2);
let cell = Self::read_stack_point(bus, sp + 6);
if let Some(state) = self.list_states.get_mut(&list_handle) {
state.cells.remove(&(cell.0, cell.1));
}
cpu.write_reg(Register::A7, sp + 10);
Ok(())
}
// LSetSelect (selector 92 / $5C)
// Selects or deselects a cell.
// PROCEDURE LSetSelect(setIt: BOOLEAN; theCell: Cell; lHandle: ListHandle);
// Inside Macintosh Volume IV, IV-273
0x5C => {
let list_handle = bus.read_long(sp + 2);
let cell = Self::read_stack_point(bus, sp + 6);
let set_it = Self::stack_bool_slot(bus, sp + 10);
let list_ptr = Self::list_record_ptr(bus, list_handle);
if let Some(state) = self.list_states.get_mut(&list_handle) {
if Self::list_cell_is_valid(state, cell.0, cell.1) {
let single_select = list_ptr != 0
&& (bus.read_byte(list_ptr + Self::LIST_SEL_FLAGS_OFFSET)
& 0x80)
!= 0;
if set_it {
if single_select {
state.selected.clear();
}
state.selected.insert((cell.0, cell.1));
} else {
state.selected.remove(&(cell.0, cell.1));
}
}
}
cpu.write_reg(Register::A7, sp + 12);
Ok(())
}
// LGetSelect (selector 60 / $3C)
// Returns whether a cell is selected, or finds the next selected cell.
// FUNCTION LGetSelect(next: BOOLEAN; VAR theCell: Cell; lHandle: ListHandle): BOOLEAN;
// Inside Macintosh Volume IV, IV-273
0x3C => {
let list_handle = bus.read_long(sp + 2);
let cell_ptr = bus.read_long(sp + 6);
let next = Self::stack_bool_slot(bus, sp + 10);
let result_addr = sp + 12;
let mut found = None;
if let Some(state) = self.list_states.get(&list_handle) {
if next {
let start = if cell_ptr != 0 {
(
bus.read_word(cell_ptr) as i16,
bus.read_word(cell_ptr + 2) as i16,
)
} else {
(state.data_bounds.0, state.data_bounds.1)
};
found = state
.selected
.iter()
.copied()
.find(|&(row, col)| (row, col) >= start);
} else if cell_ptr != 0 {
let cell = (
bus.read_word(cell_ptr) as i16,
bus.read_word(cell_ptr + 2) as i16,
);
if state.selected.contains(&cell) {
found = Some(cell);
}
}
}
if let Some(cell) = found {
if cell_ptr != 0 {
Self::write_point_words(bus, cell_ptr, cell);
}
bus.write_word(result_addr, 0x0100);
} else {
bus.write_word(result_addr, 0);
}
cpu.write_reg(Register::A7, result_addr);
Ok(())
}
// LLastClick (selector 64 / $40)
// Returns the last clicked cell; before any click the
// documented sentinel is Cell(-1, -1).
// FUNCTION LLastClick(lHandle: ListHandle): Cell;
// Inside Macintosh Volume IV, IV-273
0x40 => {
let list_handle = bus.read_long(sp + 2);
let result_addr = sp + 6;
let last_click = self
.list_states
.get(&list_handle)
.map(|state| state.last_click)
.unwrap_or_else(Self::list_no_click_cell);
Self::write_point_words(bus, result_addr, last_click);
cpu.write_reg(Register::A7, result_addr);
Ok(())
}
// LClick (selector 24 / $18)
// Tracks mouse selection in a list and returns TRUE on double-click.
// FUNCTION LClick(pt: Point; modifiers: INTEGER; lHandle: ListHandle): BOOLEAN;
// Inside Macintosh Volume IV, IV-273
0x18 => {
let list_handle = bus.read_long(sp + 2);
let _modifiers = bus.read_word(sp + 6);
let point = Self::read_stack_point(bus, sp + 8);
let result_addr = sp + 12;
let list_ptr = Self::list_record_ptr(bus, list_handle);
let mut double_click = false;
if let Some(state) = self.list_states.get_mut(&list_handle) {
if let Some(cell) = Self::list_cell_from_point(state, point) {
let single_select = list_ptr != 0
&& (bus.read_byte(list_ptr + Self::LIST_SEL_FLAGS_OFFSET)
& 0x80)
!= 0;
if single_select {
state.selected.clear();
}
state.selected.insert(cell);
double_click = state.last_click == cell
&& self.tick_count.saturating_sub(state.last_click_tick)
<= Self::LIST_DOUBLE_CLICK_TICKS;
state.last_click = cell;
state.last_click_tick = self.tick_count;
Self::sync_list_state_to_guest(bus, list_handle, state);
} else {
state.last_click = Self::list_no_click_cell();
state.last_click_tick = self.tick_count;
Self::sync_list_state_to_guest(bus, list_handle, state);
}
}
bus.write_word(result_addr, if double_click { 0x0100 } else { 0 });
cpu.write_reg(Register::A7, result_addr);
Ok(())
}
// LDraw/LUpdate/LAutoScroll/LActivate/LScroll/LSize are accepted as no-ops for now.
// Inside Macintosh Volume IV, IV-274 to IV-276
0x00 | 0x10 | 0x30 | 0x50 | 0x60 | 0x64 => {
self.pack0_fallback(cpu, bus, sp, selector)
}
// LDispose (selector 40 / $28)
// Disposes of the list.
// PROCEDURE LDispose(lHandle: ListHandle);
// Inside Macintosh Volume IV, IV-270
0x28 => {
let list_handle = bus.read_long(sp + 2);
let list_ptr = Self::list_record_ptr(bus, list_handle);
let cells_handle = if list_ptr != 0 {
bus.read_long(list_ptr + Self::LIST_CELLS_OFFSET)
} else {
0
};
self.list_states.remove(&list_handle);
if list_ptr != 0 {
bus.free(list_ptr);
}
if cells_handle != 0 {
bus.free(cells_handle);
}
if list_handle != 0 {
bus.free(list_handle);
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
_ => self.pack0_fallback(cpu, bus, sp, selector),
}
}
// Pack1 ($A9E8) — List Manager Package
//
// Twenty-five-routine selector dispatcher providing the
// List Manager API used by Standard File dialogs, font
// pickers, and any custom-list dialog. Per IM:IV-269
// explicit EQU table:
//
// lActivate $00 lAddColumn $04 lAddRow $08
// lAddToCell $0C lAutoScroll $10 lCellSize $14
// lClick $18 lClrCell $1C lDelColumn $20
// lDelRow $24 lDispose $28 lDoDraw $2C
// lDraw $30 lFind $34 lGetCell $38
// lGetSelect $3C lLastClick $40 lNew $44
// lNextCell $48 lRect $4C lScroll $50
// lSearch $54 lSetCell $58 lSetSelect $5C
// lSize $60 lUpdate $64
//
// Selectors step by FOUR (not by 2 like Pack2/3/6) because
// the Pack1 internal jump table holds 4-byte JMP entries
// per IM:IV-269. Selector encoding is pure low-byte (high
// byte $00) — same convention as Pack2 / Pack3 / Pack6,
// NOT the Pack8 / SANE param-size-in-high-byte glue.
//
// HLE compromise: Pack1 now builds and tears down real list
// records for the documented LNew/LDispose path so callers
// can obtain a live list handle. The remaining selectors
// still collapse to stack-discipline-correct no-ops until
// more Pack1 fixtures land. PROCEDUREs simply pop the
// documented Pascal frame; FUNCTIONs return defensive
// defaults (FALSE for BOOLEAN, 0 for INTEGER, (0,0) for
// Cell) when they still lack a stateful implementation.
// LSearch's searchProc trampoline is intentionally NOT
// invoked — returning FALSE is the stable "no match"
// answer for the unimplemented search path.
//
// Inside Macintosh Volume IV (1986), pages IV-259..IV-279.
// More Macintosh Toolbox Essentials (1993), 4-1..4-107.
// Pack1 / List Manager ($A9E8): Per-selector Pascal frames per IM:IV-269 EQU table: $00 LActivate pop 8, $04 LAddColumn pop 10 result@SP+10 (returns 0), $08 LAddRow pop 10 result@SP+10 (returns 0), $0C LAddToCell pop 16, $10 LAutoScroll pop 6, $14 LCellSize pop 10, $18 LClick pop 12 result@SP+12 (returns FALSE), $1C LClrCell pop 10, $20 LDelColumn pop 10, $24 LDelRow pop 10, $28 LDispose pop 6 (frees the list state), $2C LDoDraw pop 8, $30 LDraw pop 10, $34 LFind pop 18 (writes 0/0 to VAR offset/len), $38 LGetCell pop 18 (writes 0 to VAR dataLen), $3C LGetSelect pop 12 result@SP+12 (returns FALSE), $40 LLastClick pop 6 result@SP+6 (returns Cell(0,0)), $44 LNew pop 28 result@SP+28 (creates a live ListHandle), $48 LNextCell pop 14 result@SP+14 (returns FALSE), $4C LRect pop 14 (writes 0,0,0,0 to VAR cellRect), $50 LScroll pop 10, $54 LSearch pop 20 result@SP+20 (returns FALSE), $58 LSetCell pop 16, $5C LSetSelect pop 12, $60 LSize pop 10, $64 LUpdate pop 10. Unknown selector pops only the 2-byte selector word.
(true, 0x1E8) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp);
match selector {
// PROCEDURE LActivate(act: BOOLEAN;
// lHandle: ListHandle);
// IM:IV-269. Stack: sel(2) + lHandle(4) + act(2)
// = 8 bytes.
0x0000 => {
cpu.write_reg(Register::A7, sp + 8);
}
// FUNCTION LAddColumn(count, colNum: INTEGER;
// lHandle: ListHandle): INTEGER;
// IM:IV-269. Stack: sel(2) + lHandle(4) + colNum(2)
// + count(2) + result(2) = 12; pop 10,
// result@SP+10. No list, nothing added → return 0.
0x0004 => {
bus.write_word(sp + 10, 0);
cpu.write_reg(Register::A7, sp + 10);
}
// FUNCTION LAddRow(count, rowNum: INTEGER;
// lHandle: ListHandle): INTEGER;
// IM:IV-269. Same shape as LAddColumn.
0x0008 => {
bus.write_word(sp + 10, 0);
cpu.write_reg(Register::A7, sp + 10);
}
// PROCEDURE LAddToCell(dataPtr: Ptr;
// dataLen: INTEGER;
// theCell: Cell;
// lHandle: ListHandle);
// IM:IV-269 + MTb 4-82. Stack: sel(2) + lHandle(4)
// + theCell(4) + dataLen(2) + dataPtr(4) = 16.
0x000C => {
cpu.write_reg(Register::A7, sp + 16);
}
// PROCEDURE LAutoScroll(lHandle: ListHandle);
// IM:IV-269. Stack: sel(2) + lHandle(4) = 6.
0x0010 => {
cpu.write_reg(Register::A7, sp + 6);
}
// PROCEDURE LCellSize(cSize: Point;
// lHandle: ListHandle);
// IM:IV-269. Stack: sel(2) + lHandle(4) + cSize(4)
// = 10.
0x0014 => {
cpu.write_reg(Register::A7, sp + 10);
}
// FUNCTION LClick(pt: Point; modifiers: INTEGER;
// lHandle: ListHandle): BOOLEAN;
// IM:IV-269 + MTb 4-78. Stack: sel(2) + lHandle(4)
// + modifiers(2) + pt(4) + result(2) = 14; pop 12,
// result@SP+12. No list, no double-click → FALSE.
0x0018 => {
bus.write_word(sp + 12, 0);
cpu.write_reg(Register::A7, sp + 12);
}
// PROCEDURE LClrCell(theCell: Cell;
// lHandle: ListHandle);
// IM:IV-269. Stack: sel(2) + lHandle(4) + theCell(4)
// = 10.
0x001C => {
cpu.write_reg(Register::A7, sp + 10);
}
// PROCEDURE LDelColumn(count, colNum: INTEGER;
// lHandle: ListHandle);
// IM:IV-269. Stack: sel(2) + lHandle(4) + colNum(2)
// + count(2) = 10.
0x0020 => {
cpu.write_reg(Register::A7, sp + 10);
}
// PROCEDURE LDelRow(count, rowNum: INTEGER;
// lHandle: ListHandle);
// IM:IV-269. Same shape as LDelColumn.
0x0024 => {
cpu.write_reg(Register::A7, sp + 10);
}
// PROCEDURE LDispose(lHandle: ListHandle);
// IM:IV-269. Stack: sel(2) + lHandle(4) = 6.
0x0028 => {
let list_handle = bus.read_long(sp + 2);
let list_ptr = Self::list_record_ptr(bus, list_handle);
let cells_handle = if list_ptr != 0 {
bus.read_long(list_ptr + Self::LIST_CELLS_OFFSET)
} else {
0
};
self.list_states.remove(&list_handle);
if list_ptr != 0 {
bus.free(list_ptr);
}
if cells_handle != 0 {
bus.free(cells_handle);
}
if list_handle != 0 {
bus.free(list_handle);
}
cpu.write_reg(Register::A7, sp + 6);
}
// PROCEDURE LDoDraw(drawIt: BOOLEAN;
// lHandle: ListHandle);
// IM:IV-269 + MTb 4-83 (alias LSetDrawingMode).
// Stack: sel(2) + lHandle(4) + drawIt(2) = 8.
0x002C => {
cpu.write_reg(Register::A7, sp + 8);
}
// PROCEDURE LDraw(theCell: Cell;
// lHandle: ListHandle);
// IM:IV-269. Stack: sel(2) + lHandle(4) + theCell(4)
// = 10.
0x0030 => {
cpu.write_reg(Register::A7, sp + 10);
}
// PROCEDURE LFind(VAR offset, len: INTEGER;
// theCell: Cell;
// lHandle: ListHandle);
// IM:IV-263, 269. Pascal pushes args left-to-right
// (first pushed = deepest), so source order
// offset, len, theCell, lHandle places lHandle
// (last) at sp+2, theCell at sp+6, len ptr at
// sp+10, offset ptr at sp+14. VAR INTEGER args
// are 4-byte ptrs each. Stack: sel(2) + lHandle(4)
// + theCell(4) + len ptr(4) + offset ptr(4) = 18.
// No list → write 0 to both *offset and *len.
0x0034 => {
let len_ptr = bus.read_long(sp + 10);
let offset_ptr = bus.read_long(sp + 14);
if offset_ptr != 0 {
bus.write_word(offset_ptr, 0);
}
if len_ptr != 0 {
bus.write_word(len_ptr, 0);
}
cpu.write_reg(Register::A7, sp + 18);
}
// PROCEDURE LGetCell(dataPtr: Ptr;
// VAR dataLen: INTEGER;
// theCell: Cell;
// lHandle: ListHandle);
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + theCell(4) + dataLen ptr(4) + dataPtr(4) = 18.
// No cell data → write 0 to *dataLen.
0x0038 => {
let datalen_ptr = bus.read_long(sp + 10);
if datalen_ptr != 0 {
bus.write_word(datalen_ptr, 0);
}
cpu.write_reg(Register::A7, sp + 18);
}
// FUNCTION LGetSelect(next: BOOLEAN;
// VAR theCell: Cell;
// lHandle: ListHandle): BOOLEAN;
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + theCell ptr(4) + next(2) + result(2) = 14;
// pop 12, result@SP+12. No selection → FALSE.
0x003C => {
bus.write_word(sp + 12, 0);
cpu.write_reg(Register::A7, sp + 12);
}
// FUNCTION LLastClick(lHandle: ListHandle): Cell;
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + result(4) = 10; pop 6, result@SP+6 (Cell long).
// No prior click → write Cell(-1,-1) = 0xFFFF_FFFF.
0x0040 => {
bus.write_long(sp + 6, 0xFFFF_FFFF);
cpu.write_reg(Register::A7, sp + 6);
}
// FUNCTION LNew(rView, dataBounds: Rect;
// cSize: Point; theProc: INTEGER;
// theWindow: WindowPtr;
// drawIt, hasGrow, scrollHoriz,
// scrollVert: BOOLEAN): ListHandle;
// IM:IV-269 + MTb 4-70. Rects pass by REFERENCE
// (4-byte ptr) per the QuickDraw Toolbox-wide
// convention reaffirmed by PtInRect at
// quickdraw.rs:1654. Stack: sel(2) + scrollVert(2)
// + scrollHoriz(2) + hasGrow(2) + drawIt(2)
// + theWindow(4) + theProc(2) + cSize(4)
// + dataBounds ptr(4) + rView ptr(4) + result(4)
// = 32; pop 28, result@SP+28 (long, ListHandle).
// Pack1 now mirrors the Pack0 list-record setup so
// callers can obtain a live list handle.
0x0044 => {
let draw_it = Self::stack_bool_slot(bus, sp + 2);
let has_grow = Self::stack_bool_slot(bus, sp + 4);
let scroll_h = Self::stack_bool_slot(bus, sp + 6);
let scroll_v = Self::stack_bool_slot(bus, sp + 8);
let window = bus.read_long(sp + 10);
let proc_id = bus.read_word(sp + 14) as i16;
let cell_size = Self::read_stack_point(bus, sp + 16);
let data_bounds_ptr = bus.read_long(sp + 20);
let view_rect_ptr = bus.read_long(sp + 24);
let result_addr = sp + 28;
let view_rect = Self::read_rect_ptr(bus, view_rect_ptr);
let data_bounds = Self::read_rect_ptr(bus, data_bounds_ptr);
let resolved_cell_size =
self.compute_list_cell_size(view_rect, data_bounds, cell_size);
let visible = Self::compute_list_visible_rect(
view_rect,
data_bounds,
resolved_cell_size,
);
let list_ptr = bus.alloc(Self::LIST_RECORD_SIZE);
let list_handle = bus.alloc(4);
let cells_handle = bus.alloc(4);
let list_def_proc_handle = self
.find_resource_any(*b"LDEF", proc_id)
.map(|(_, ptr)| {
self.get_or_create_resource_handle(bus, *b"LDEF", proc_id, ptr)
})
.unwrap_or(0);
if list_handle != 0 {
bus.write_long(list_handle, list_ptr);
}
if cells_handle != 0 {
bus.write_long(cells_handle, 0);
}
let state = super::dispatch::ListState {
view_rect,
data_bounds,
cell_size: resolved_cell_size,
visible,
port: window,
draw_enabled: draw_it,
cells: std::collections::HashMap::new(),
selected: std::collections::BTreeSet::new(),
last_click: Self::list_no_click_cell(),
last_click_tick: 0,
};
if list_ptr != 0 {
Self::write_rect_words(
bus,
list_ptr + Self::LIST_RVIEW_OFFSET,
view_rect,
);
bus.write_long(list_ptr + Self::LIST_PORT_OFFSET, window);
Self::write_point_words(
bus,
list_ptr + Self::LIST_INDENT_OFFSET,
(0, 0),
);
Self::write_point_words(
bus,
list_ptr + Self::LIST_CELL_SIZE_OFFSET,
resolved_cell_size,
);
Self::write_rect_words(
bus,
list_ptr + Self::LIST_VISIBLE_OFFSET,
visible,
);
bus.write_long(list_ptr + Self::LIST_VSCROLL_OFFSET, 0);
bus.write_long(list_ptr + Self::LIST_HSCROLL_OFFSET, 0);
bus.write_byte(list_ptr + Self::LIST_SEL_FLAGS_OFFSET, 0);
bus.write_byte(list_ptr + Self::LIST_ACTIVE_OFFSET, 1);
bus.write_byte(list_ptr + Self::LIST_RESERVED_OFFSET, 0);
bus.write_byte(list_ptr + Self::LIST_FLAGS_OFFSET, 0);
bus.write_long(list_ptr + Self::LIST_CLICK_TIME_OFFSET, 0);
Self::write_point_words(
bus,
list_ptr + Self::LIST_CLICK_LOC_OFFSET,
(-32768, -32768),
);
Self::write_point_words(
bus,
list_ptr + Self::LIST_MOUSE_LOC_OFFSET,
(-1, -1),
);
bus.write_long(list_ptr + Self::LIST_CLICK_LOOP_OFFSET, 0);
Self::write_point_words(
bus,
list_ptr + Self::LIST_LAST_CLICK_OFFSET,
(-1, -1),
);
bus.write_long(list_ptr + Self::LIST_REFCON_OFFSET, 0);
bus.write_long(
list_ptr + Self::LIST_DEF_PROC_OFFSET,
list_def_proc_handle,
);
bus.write_long(list_ptr + Self::LIST_USER_HANDLE_OFFSET, 0);
Self::write_rect_words(
bus,
list_ptr + Self::LIST_DATA_BOUNDS_OFFSET,
data_bounds,
);
bus.write_long(list_ptr + Self::LIST_CELLS_OFFSET, cells_handle);
let rows = (data_bounds.2 - data_bounds.0).max(0) as i32;
let cols = (data_bounds.3 - data_bounds.1).max(0) as i32;
bus.write_word(
list_ptr + Self::LIST_MAX_INDEX_OFFSET,
rows.saturating_mul(cols).saturating_mul(2) as u16,
);
bus.write_word(list_ptr + Self::LIST_CELL_ARRAY_OFFSET, 0);
}
if list_handle != 0 {
self.list_states.insert(list_handle, state);
}
if trace_list_manager_enabled() {
eprintln!(
"[LIST] LNew handle=${:08X} ptr=${:08X} proc={} draw={} grow={} scroll_h={} scroll_v={} view=({},{},{},{}) bounds=({},{},{},{}) cell=({}, {})",
list_handle,
list_ptr,
proc_id,
draw_it,
has_grow,
scroll_h,
scroll_v,
view_rect.0,
view_rect.1,
view_rect.2,
view_rect.3,
data_bounds.0,
data_bounds.1,
data_bounds.2,
data_bounds.3,
resolved_cell_size.0,
resolved_cell_size.1,
);
}
bus.write_long(result_addr, list_handle);
cpu.write_reg(Register::A7, result_addr);
}
// FUNCTION LNextCell(hNext, vNext: BOOLEAN;
// VAR theCell: Cell;
// lHandle: ListHandle): BOOLEAN;
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + theCell ptr(4) + vNext(2) + hNext(2)
// + result(2) = 16; pop 14, result@SP+14.
// No list → FALSE.
0x0048 => {
bus.write_word(sp + 14, 0);
cpu.write_reg(Register::A7, sp + 14);
}
// PROCEDURE LRect(VAR cellRect: Rect;
// theCell: Cell;
// lHandle: ListHandle);
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + theCell(4) + cellRect ptr(4) = 14.
// No list → write empty Rect (0,0,0,0).
0x004C => {
let rect_ptr = bus.read_long(sp + 10);
if rect_ptr != 0 {
bus.write_word(rect_ptr, 0);
bus.write_word(rect_ptr + 2, 0);
bus.write_word(rect_ptr + 4, 0);
bus.write_word(rect_ptr + 6, 0);
}
cpu.write_reg(Register::A7, sp + 14);
}
// PROCEDURE LScroll(dCols, dRows: INTEGER;
// lHandle: ListHandle);
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + dRows(2) + dCols(2) = 10.
0x0050 => {
cpu.write_reg(Register::A7, sp + 10);
}
// FUNCTION LSearch(dataPtr: Ptr; dataLen: INTEGER;
// searchProc: Ptr;
// VAR theCell: Cell;
// lHandle: ListHandle): BOOLEAN;
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + theCell ptr(4) + searchProc(4) + dataLen(2)
// + dataPtr(4) + result(2) = 22; pop 20,
// result@SP+20. No list → FALSE; do NOT invoke
// the searchProc trampoline.
0x0054 => {
bus.write_word(sp + 20, 0);
cpu.write_reg(Register::A7, sp + 20);
}
// PROCEDURE LSetCell(dataPtr: Ptr;
// dataLen: INTEGER;
// theCell: Cell;
// lHandle: ListHandle);
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + theCell(4) + dataLen(2) + dataPtr(4) = 16.
0x0058 => {
cpu.write_reg(Register::A7, sp + 16);
}
// PROCEDURE LSetSelect(setIt: BOOLEAN;
// theCell: Cell;
// lHandle: ListHandle);
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + theCell(4) + setIt(2) = 12.
0x005C => {
cpu.write_reg(Register::A7, sp + 12);
}
// PROCEDURE LSize(listWidth, listHeight: INTEGER;
// lHandle: ListHandle);
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + listHeight(2) + listWidth(2) = 10.
0x0060 => {
cpu.write_reg(Register::A7, sp + 10);
}
// PROCEDURE LUpdate(theRgn: RgnHandle;
// lHandle: ListHandle);
// IM:IV-263, 269. Stack: sel(2) + lHandle(4)
// + theRgn(4) = 10.
0x0064 => {
cpu.write_reg(Register::A7, sp + 10);
}
_ => {
// Unknown / undocumented selector — pop just
// the 2-byte selector word so the caller's
// stack stays balanced.
cpu.write_reg(Register::A7, sp + 2);
}
}
cpu.write_reg(Register::D0, 0);
Ok(())
}
// Pack2 ($A9E9) — Disk Initialization Manager
//
// Six-routine selector-based dispatcher for floppy / SCSI
// disk formatting (DIBadMount, DILoad, DIUnload, DIFormat,
// DIVerify, DIZero). The selector word sits at SP+0; for
// FUNCTIONs the caller pre-allocated a 2-byte INTEGER /
// OSErr result slot below the args (deepest on the stack).
//
// Systemless's HLE mounts a single fixed VFS volume and never
// synthesises diskInsertedEvents, so every routine collapses
// to a noErr no-op while still popping the documented Pascal
// stack frame and writing 0 = noErr to the FUNCTION result
// slot. Pop sizes per IM:Files 5-15..5-21 + selector summary
// 5-24.
//
// Inside Macintosh: Files (1992), Chapter 5 "Disk
// Initialization Manager", pages 5-15..5-21.
// Pack2 / DiskInit ($A9E9): Per-selector Pascal frames per IM:Files 5-15..5-21: $0000 DIBadMount(Point,LongInt):Integer pops 8+selector, $0002 DILoad / $0004 DIUnload pop just selector, $0006 DIFormat / $0008 DIVerify (Integer):OSErr pop 2+selector, $000A DIZero(Integer,Str255):OSErr pops 258+selector (Pascal Str255 by-value). All collapse to noErr — single VFS volume, never sees disk-insert events.
(true, 0x1E9) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp);
let (arg_bytes, has_result) = match selector {
// FUNCTION DIBadMount(where: Point; evtMessage: LongInt): Integer
// IM:Files 5-18..5-19. where=4 (Point by value),
// evtMessage=4 (LongInt by value). Returns 0 = "no
// error / user proceeded" — the only result code that
// never causes a caller to escalate to DoError.
0x0000 => (8u32, true),
// PROCEDURE DILoad / PROCEDURE DIUnload — no args, no
// result. IM:Files 5-15..5-16. The Disk Init Manager
// is always "loaded" in our HLE so both are no-ops.
0x0002 | 0x0004 => (0u32, false),
// FUNCTION DIFormat(drvNum: Integer): OSErr
// FUNCTION DIVerify(drvNum: Integer): OSErr
// IM:Files 5-19..5-20. Both return noErr on the
// single VFS volume.
0x0006 | 0x0008 => (2u32, true),
// FUNCTION DIZero(drvNum: Integer; volName: Str255): OSErr
// IM:Files 5-21. Pascal Str255 is pushed by value
// (256 bytes); MPW C glue marshals from the
// ConstStr255Param pointer into a stack-local
// Str255 before invoking the trap. Total args:
// drvNum(2) + Str255(256) = 258.
0x000A => (258u32, true),
_ => {
// No other selectors in IM:Files 5-24 summary.
// Pop just the selector and return noErr; future
// System additions would land in a new arm here.
cpu.write_reg(Register::A7, sp + 2);
cpu.write_reg(Register::D0, 0);
return Some(Ok(()));
}
};
let pop_total = 2 + arg_bytes;
if has_result {
bus.write_word(sp + pop_total, 0);
}
cpu.write_reg(Register::A7, sp + pop_total);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// Pack3 ($A9EA) — Standard File Package
//
// Eight-routine selector dispatcher for the system Open /
// Save dialogs. The selector word sits at SP+0; ALL eight
// routines are PROCEDUREs (no FUNCTION result slot). Each
// writes a Pascal record (SFReply for $0001..$0004,
// StandardFileReply for $0005..$0008) into a VAR reply
// pointer that the caller pushed by reference.
//
// Systemless has no native file-picker UI and never displays
// a modal SF dialog, so every routine collapses to the
// documented "user canceled" path: write 0 to the reply
// record's good / sfGood byte (offset 0 in both record
// types per IM:Files 3-61) and pop the documented Pascal
// frame. Apps that follow the IM:I I-518 idiom
// SFGetFile(...); IF reply.good THEN ProceedWithFile
// gracefully fall through. The rest of the reply record
// is left untouched — callers must not read past .good
// when good = FALSE per IM:Files 3-61 contract.
//
// Selector encoding is pure low-byte routine number
// (high byte $00) per IM:Files 3-45..3-54 explicit
// "Selector: $0005" tables — this is NOT the Pack8/SANE
// param-size-in-high-byte convention.
//
// Stack frames assume Str255 args are pushed BY REFERENCE
// (4-byte pointer), the modern MPW Toolbox convention
// matching AppendMenu / InsertMenuItem / SetWTitle and
// the ConstStr255Param C binding type. Pack2 DIZero's
// Pascal-by-value 256-byte convention is a separate read
// of an isolated routine.
//
// Inside Macintosh: Files (1992), Chapter 3 "Standard
// File Package", pages 3-43..3-61.
// Inside Macintosh Volume I (1985), pages I-518..I-527.
// Inside Macintosh Volume VI (1991), pages 26-21..26-25
// (CustomGetFile / CustomPutFile additions).
// Pack3 / Standard File ($A9EA): Per-selector Pascal frames per IM:Files 3-45..3-54: $0001 SFPutFile pop 22 reply@SP+2, $0002 SFGetFile pop 28 reply@SP+2, $0003 SFPPutFile pop 28 reply@SP+8, $0004 SFPGetFile pop 34 reply@SP+8, $0005 StandardPutFile pop 14 reply@SP+2, $0006 StandardGetFile pop 16 reply@SP+2, $0007 CustomPutFile pop 40 reply@SP+28, $0008 CustomGetFile pop 42 reply@SP+28. Each writes 0 to reply.good / sfGood (offset 0) per the documented "user canceled" semantic — Systemless has no native file-picker UI.
(true, 0x1EA) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp);
let (arg_bytes, reply_offset) = match selector {
// PROCEDURE SFPutFile(where: Point; prompt: Str255;
// origName: Str255;
// dlgHook: ProcPtr;
// VAR reply: SFReply);
// IM:Files 3-52 / IM:I I-519..I-522. Reply ptr is
// last Pascal arg → at SP+2 above the selector.
0x0001 => (20u32, 2u32),
// PROCEDURE SFGetFile(where: Point; prompt: Str255;
// fileFilter: ProcPtr;
// numTypes: Integer;
// typeList: SFTypeList;
// dlgHook: ProcPtr;
// VAR reply: SFReply);
// IM:Files 3-52..3-53 / IM:I I-523..I-526.
0x0002 => (26u32, 2u32),
// PROCEDURE SFPPutFile(...; VAR reply: SFReply;
// dlgID: Integer;
// filterProc: ProcPtr);
// IM:I I-522..I-523. filterProc(4) + dlgID(2) sit
// ABOVE reply ptr on the stack → reply at SP+8.
0x0003 => (26u32, 8u32),
// PROCEDURE SFPGetFile(...; VAR reply: SFReply;
// dlgID: Integer;
// filterProc: ProcPtr);
// IM:I I-526..I-527.
0x0004 => (32u32, 8u32),
// PROCEDURE StandardPutFile(prompt: Str255;
// defaultName: Str255;
// VAR reply:
// StandardFileReply);
// IM:Files 3-45.
0x0005 => (12u32, 2u32),
// PROCEDURE StandardGetFile(fileFilter: ProcPtr;
// numTypes: Integer;
// typeList: SFTypeList;
// VAR reply:
// StandardFileReply);
// IM:Files 3-50.
0x0006 => (14u32, 2u32),
// PROCEDURE CustomPutFile(prompt: Str255;
// defaultName: Str255;
// VAR reply:
// StandardFileReply;
// dlgID: Integer;
// where: Point;
// dlgHook: ProcPtr;
// filterProc: ProcPtr;
// activeList: Ptr;
// activateProc: ProcPtr;
// yourDataPtr: UNIV Ptr);
// IM:Files 3-46 / IM:VI 26-21. yourData(4) +
// activate(4) + activeList(4) + filter(4) +
// dlgHook(4) + where(4) + dlgID(2) = 26 bytes
// ABOVE reply → reply at SP+28.
0x0007 => (38u32, 28u32),
// PROCEDURE CustomGetFile(fileFilter: ProcPtr;
// numTypes: Integer;
// typeList: SFTypeList;
// VAR reply:
// StandardFileReply;
// dlgID: Integer;
// where: Point;
// dlgHook: ProcPtr;
// filterProc: ProcPtr;
// activeList: Ptr;
// activateProc: ProcPtr;
// yourDataPtr: UNIV Ptr);
// IM:Files 3-51 / IM:VI 26-22.
0x0008 => (40u32, 28u32),
_ => {
// No other selectors documented in IM:Files
// 3-45..3-54 or IM:I I-518..I-527. Pop just
// the selector word defensively so a future
// System addition doesn't corrupt the caller
// stack.
cpu.write_reg(Register::A7, sp + 2);
cpu.write_reg(Register::D0, 0);
return Some(Ok(()));
}
};
let pop_total = 2 + arg_bytes;
let reply_ptr = bus.read_long(sp + reply_offset);
standard_file_cancel_reply(bus, reply_ptr);
cpu.write_reg(Register::A7, sp + pop_total);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// Pack4 ($A9EB) and Pack5 ($A9EC) are handled by dispatch_sane
// Pack6 ($A9ED) — International Utilities Package
//
// Eighteen-routine selector dispatcher providing date/time
// formatting, international string comparison, and (System
// 7+) interscript ordering / itl2/itl4 cache management.
//
// Selector encoding is pure low-byte routine number (high
// byte $00) per IM:I I-487 + IM:VI 14-135 explicit
// "Selector: $XXXX" tables — same convention as Pack2 /
// Pack3, NOT the Pack8 / SANE param-size-in-high-byte
// convention.
//
// HLE compromise: Systemless runs a single-script (Roman /
// Latin-1) US-default environment with no localised
// 'INTL' / 'itl2' / 'itl4' resources. Date strings
// collapse to a "1/1/04" placeholder; time strings to
// "12:00 AM" / "12:00:00 AM"; metric query returns FALSE;
// INTL resource handles return NIL; comparators do byte-
// level compare returning -1/0/+1 (Mag/Comp variants;
// case-sensitive) or 0/1 (MagID/Equal variants; case-
// insensitive ASCII fold); script/lang ordering does
// numeric compare. IUClearCache / IUSetIntl /
// IUGetIntlTable are no-ops with documented VAR-out NIL
// writes. Apps that defensively check (handle != NIL)
// before dereffing fall through cleanly; apps that need
// locale-specific formatting see Mac-default English
// output. The "no INTL" fallback is INTENTIONALLY SAFE
// per IM:I I-505 ("if the INTL resource is missing,
// IUGetIntl returns NIL").
//
// Stack frames assume Pascal arg conventions: LongInt =
// 4, Integer/Boolean/DateForm = 2 (DateForm = 1 byte at
// the source level but stack-aligned to 2 bytes), Handle
// / Ptr = 4, Str255 by REFERENCE (4-byte VAR ptr,
// matching the Toolbox-wide convention reaffirmed by
// Pack3, AppendMenu, SetWTitle, and
// ParamText). VAR LongDateTime is also a 4-byte ptr.
//
// IUCompString / IUEqualString / IUCompPString /
// IUEqualPString are pure Pascal-glue convenience wrappers
// (per IM:I I-498 "there's no trap for it; it eventually
// calls IUMagString" / IM:I I-501 same for IUEqualString)
// — they have NO selector and reach this dispatcher via
// their Mag / MagID counterparts.
//
// Inside Macintosh Volume I (1985), pages I-485..I-510.
// Inside Macintosh Volume VI (1991), pages 14-1..14-135.
// Pack6 / Intl Utilities ($A9ED): Per-selector Pascal frames per IM:I I-487 + IM:VI 14-135: $0000 IUDateString pop 12 result@SP+2, $0002 IUTimeString pop 12 result@SP+2, $0004 IUMetric pop 2 result@SP+2 (FALSE), $0006 IUGetIntl pop 4 result@SP+4 (NIL handle), $0008 IUSetIntl pop 10 (no-op), $000A IUMagString pop 14 result@SP+14 (-1/0/+1 byte cmp), $000C IUMagIDString pop 14 result@SP+14 (0/1 case-insens), $000E IUDatePString pop 16 result@SP+6, $0010 IUTimePString pop 16 result@SP+6, $0014 IULDateString pop 16 result@SP+6, $0016 IULTimeString pop 16 result@SP+6, $0018 IUClearCache pop 2 (no-op), $001A IUMagPString pop 18 result@SP+18 (-1/0/+1), $001C IUMagIDPString pop 18 result@SP+18 (0/1), $001E IUScriptOrder pop 6 result@SP+6 (-1/0/+1), $0020 IULangOrder pop 6 result@SP+6 (-1/0/+1), $0022 IUTextOrder pop 22 result@SP+22 (-1/0/+1), $0024 IUGetIntlTable pop 18 (writes NIL/0/0 to 3 VAR ptrs).
(true, 0x1ED) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp);
match selector {
// PROCEDURE IUDateString(dateTime: LongInt;
// form: DateForm;
// VAR result: Str255);
// IM:I I-487, I-504. Stack: sel(2) + result(4)
// + form(2) + dateTime(4) = 12.
0x0000 => {
let result_ptr = bus.read_long(sp + 2);
if result_ptr != 0 {
bus.write_pstring(result_ptr, b"1/1/04");
}
cpu.write_reg(Register::A7, sp + 12);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PROCEDURE IUTimeString(dateTime: LongInt;
// wantSeconds: Boolean;
// VAR result: Str255);
// IM:I I-487, I-504. Stack: sel(2) + result(4)
// + wantSec(2) + dateTime(4) = 12.
0x0002 => {
let result_ptr = bus.read_long(sp + 2);
let want_seconds = bus.read_word(sp + 6) != 0;
if result_ptr != 0 {
let s: &[u8] = if want_seconds {
b"12:00:00 AM"
} else {
b"12:00 AM"
};
bus.write_pstring(result_ptr, s);
}
cpu.write_reg(Register::A7, sp + 12);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// FUNCTION IUMetric: Boolean;
// IM:I I-487, I-505. Stack: sel(2) + result(2)
// = 4. Pop 2, leave result word at new SP+0.
0x0004 => {
bus.write_word(sp + 2, 0);
cpu.write_reg(Register::A7, sp + 2);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// FUNCTION IUGetIntl(theID: Integer): Handle;
// IM:I I-487, I-505. Stack: sel(2) + theID(2)
// + result(4) = 8. Pop 4, leave result long at
// new SP+0. Returns NIL — no INTL resources.
0x0006 => {
bus.write_long(sp + 4, 0);
cpu.write_reg(Register::A7, sp + 4);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PROCEDURE IUSetIntl(refNum: Integer;
// theID: Integer;
// intlParam: Handle);
// IM:I I-487, I-503. Stack: sel(2) +
// intlParam(4) + theID(2) + refNum(2) = 10.
// No-op (HLE doesn't track INTL overrides).
0x0008 => {
cpu.write_reg(Register::A7, sp + 10);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// FUNCTION IUMagString(aPtr,bPtr: Ptr;
// aLen,bLen: Integer):
// Integer;
// IM:I I-487, I-507. Stack: sel(2) + bLen(2) +
// aLen(2) + bPtr(4) + aPtr(4) + result(2) = 16.
// Pop 14, leave result at new SP+0.
0x000A => {
let a_ptr = bus.read_long(sp + 10);
let b_ptr = bus.read_long(sp + 6);
let a_len = bus.read_word(sp + 4) as usize;
let b_len = bus.read_word(sp + 2) as usize;
let a = bus.read_bytes(a_ptr, a_len);
let b = bus.read_bytes(b_ptr, b_len);
let result: i16 = match a.cmp(&b) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
};
bus.write_word(sp + 14, result as u16);
cpu.write_reg(Register::A7, sp + 14);
cpu.write_reg(Register::D0, result as u16 as u32);
Ok(())
}
// FUNCTION IUMagIDString(aPtr,bPtr: Ptr;
// aLen,bLen: Integer):
// Integer;
// IM:I I-487, I-507. Same stack as IUMagString.
// Returns 0 (case-insensitive equal) or 1 (not).
0x000C => {
let a_ptr = bus.read_long(sp + 10);
let b_ptr = bus.read_long(sp + 6);
let a_len = bus.read_word(sp + 4) as usize;
let b_len = bus.read_word(sp + 2) as usize;
let a: Vec<u8> = bus
.read_bytes(a_ptr, a_len)
.iter()
.map(|c| c.to_ascii_lowercase())
.collect();
let b: Vec<u8> = bus
.read_bytes(b_ptr, b_len)
.iter()
.map(|c| c.to_ascii_lowercase())
.collect();
let result: u16 = if a == b { 0 } else { 1 };
bus.write_word(sp + 14, result);
cpu.write_reg(Register::A7, sp + 14);
cpu.write_reg(Register::D0, result as u32);
Ok(())
}
// PROCEDURE IUDatePString(dateTime: LongInt;
// form: DateForm;
// VAR result: Str255;
// intlParam: Handle);
// IM:I I-487, I-505. Stack: sel(2) +
// intlParam(4) + result(4) + form(2) +
// dateTime(4) = 16.
0x000E => {
let result_ptr = bus.read_long(sp + 6);
if result_ptr != 0 {
bus.write_pstring(result_ptr, b"1/1/04");
}
cpu.write_reg(Register::A7, sp + 16);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PROCEDURE IUTimePString(dateTime: LongInt;
// wantSeconds: Boolean;
// VAR result: Str255;
// intlParam: Handle);
// IM:I I-487, I-505. Stack: sel(2) +
// intlParam(4) + result(4) + wantSec(2) +
// dateTime(4) = 16.
0x0010 => {
let result_ptr = bus.read_long(sp + 6);
let want_seconds = bus.read_word(sp + 10) != 0;
if result_ptr != 0 {
let s: &[u8] = if want_seconds {
b"12:00:00 AM"
} else {
b"12:00 AM"
};
bus.write_pstring(result_ptr, s);
}
cpu.write_reg(Register::A7, sp + 16);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PROCEDURE IULDateString(VAR dateTime:
// LongDateTime;
// longFlag: DateForm;
// VAR Result: Str255;
// intlParam: Handle);
// IM:VI 14-135. Stack: sel(2) + intlParam(4) +
// result(4) + longFlag(2) + dateTime ptr(4) = 16.
0x0014 => {
let result_ptr = bus.read_long(sp + 6);
if result_ptr != 0 {
bus.write_pstring(result_ptr, b"1/1/04");
}
cpu.write_reg(Register::A7, sp + 16);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PROCEDURE IULTimeString(VAR dateTime:
// LongDateTime;
// wantSeconds: Boolean;
// VAR Result: Str255;
// intlParam: Handle);
// IM:VI 14-135. Stack: sel(2) + intlParam(4) +
// result(4) + wantSec(2) + dateTime ptr(4) = 16.
0x0016 => {
let result_ptr = bus.read_long(sp + 6);
let want_seconds = bus.read_word(sp + 10) != 0;
if result_ptr != 0 {
let s: &[u8] = if want_seconds {
b"12:00:00 AM"
} else {
b"12:00 AM"
};
bus.write_pstring(result_ptr, s);
}
cpu.write_reg(Register::A7, sp + 16);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// PROCEDURE IUClearCache;
// IM:VI 14-76. Stack: sel(2) only. No-op (HLE
// has no IUtil cache).
0x0018 => {
cpu.write_reg(Register::A7, sp + 2);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// FUNCTION IUMagPString(aPtr,bPtr: Ptr;
// aLen,bLen: Integer;
// itl2Handle: Handle):
// Integer;
// IM:VI 14-135. Stack: sel(2) + itl2(4) +
// bLen(2) + aLen(2) + bPtr(4) + aPtr(4) +
// result(2) = 20. Pop 18, leave result at new
// SP+0. itl2Handle ignored (HLE has no itl2).
0x001A => {
let a_ptr = bus.read_long(sp + 14);
let b_ptr = bus.read_long(sp + 10);
let a_len = bus.read_word(sp + 8) as usize;
let b_len = bus.read_word(sp + 6) as usize;
let a = bus.read_bytes(a_ptr, a_len);
let b = bus.read_bytes(b_ptr, b_len);
let result: i16 = match a.cmp(&b) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
};
bus.write_word(sp + 18, result as u16);
cpu.write_reg(Register::A7, sp + 18);
cpu.write_reg(Register::D0, result as u16 as u32);
Ok(())
}
// FUNCTION IUMagIDPString(aPtr,bPtr: Ptr;
// aLen,bLen: Integer;
// itl2Handle: Handle):
// Integer;
// IM:VI 14-135. Same stack as IUMagPString.
0x001C => {
let a_ptr = bus.read_long(sp + 14);
let b_ptr = bus.read_long(sp + 10);
let a_len = bus.read_word(sp + 8) as usize;
let b_len = bus.read_word(sp + 6) as usize;
let a: Vec<u8> = bus
.read_bytes(a_ptr, a_len)
.iter()
.map(|c| c.to_ascii_lowercase())
.collect();
let b: Vec<u8> = bus
.read_bytes(b_ptr, b_len)
.iter()
.map(|c| c.to_ascii_lowercase())
.collect();
let result: u16 = if a == b { 0 } else { 1 };
bus.write_word(sp + 18, result);
cpu.write_reg(Register::A7, sp + 18);
cpu.write_reg(Register::D0, result as u32);
Ok(())
}
// FUNCTION IUScriptOrder(script1, script2:
// ScriptCode): Integer;
// IM:VI 14-135. Stack: sel(2) + script2(2) +
// script1(2) + result(2) = 8. Pop 6, leave
// result at new SP+0.
0x001E => {
let script1 = bus.read_word(sp + 4) as i16;
let script2 = bus.read_word(sp + 2) as i16;
let result: i16 = match script1.cmp(&script2) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
};
bus.write_word(sp + 6, result as u16);
cpu.write_reg(Register::A7, sp + 6);
cpu.write_reg(Register::D0, result as u16 as u32);
Ok(())
}
// FUNCTION IULangOrder(language1, language2:
// LangCode): Integer;
// IM:VI 14-135. Same stack as IUScriptOrder.
0x0020 => {
let lang1 = bus.read_word(sp + 4) as i16;
let lang2 = bus.read_word(sp + 2) as i16;
let result: i16 = match lang1.cmp(&lang2) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
};
bus.write_word(sp + 6, result as u16);
cpu.write_reg(Register::A7, sp + 6);
cpu.write_reg(Register::D0, result as u16 as u32);
Ok(())
}
// FUNCTION IUTextOrder(aPtr,bPtr: Ptr;
// aLen,bLen: Integer;
// aScript,bScript:
// ScriptCode;
// aLang,bLang: LangCode):
// Integer;
// IM:VI 14-135. Stack: sel(2) + bLang(2) +
// aLang(2) + bScript(2) + aScript(2) + bLen(2)
// + aLen(2) + bPtr(4) + aPtr(4) + result(2) =
// 24. Pop 22, leave result at new SP+0. All
// script/lang args ignored in single-script HLE.
0x0022 => {
let a_ptr = bus.read_long(sp + 18);
let b_ptr = bus.read_long(sp + 14);
let a_len = bus.read_word(sp + 12) as usize;
let b_len = bus.read_word(sp + 10) as usize;
let a = bus.read_bytes(a_ptr, a_len);
let b = bus.read_bytes(b_ptr, b_len);
let result: i16 = match a.cmp(&b) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
};
bus.write_word(sp + 22, result as u16);
cpu.write_reg(Register::A7, sp + 22);
cpu.write_reg(Register::D0, result as u16 as u32);
Ok(())
}
// PROCEDURE IUGetIntlTable(script: ScriptCode;
// tableCode: Integer;
// VAR itlHandle: Handle;
// VAR offset: LongInt;
// VAR length: LongInt);
// IM:VI 14-135. Stack: sel(2) + length(4) +
// offset(4) + itlHandle(4) + tableCode(2) +
// script(2) = 18. No itl2/itl4 tables in HLE
// — write NIL/0/0 to all three VAR ptrs so
// caller's defensive (handle == NIL) check
// sends them down the "table not available"
// path.
0x0024 => {
let length_ptr = bus.read_long(sp + 2);
let offset_ptr = bus.read_long(sp + 6);
let handle_ptr = bus.read_long(sp + 10);
if handle_ptr != 0 {
bus.write_long(handle_ptr, 0);
}
if offset_ptr != 0 {
bus.write_long(offset_ptr, 0);
}
if length_ptr != 0 {
bus.write_long(length_ptr, 0);
}
cpu.write_reg(Register::A7, sp + 18);
cpu.write_reg(Register::D0, 0);
Ok(())
}
_ => {
// Defensive fallback: pop only the selector
// word so a future System addition or buggy
// caller doesn't corrupt the rest of the
// stack. Documented selectors are $0000..
// $0010 (even) per IM:I I-487 plus $0014..
// $0024 (even) per IM:VI 14-135. Selector
// $0012 is unused — IM:VI's enumeration at
// 14-135 jumps from $0010 IUTimePString
// straight to $0014 IULDateString.
cpu.write_reg(Register::A7, sp + 2);
cpu.write_reg(Register::D0, 0);
Ok(())
}
}
}
// Pack7 ($A9EE) — Binary/Decimal Conversion Package
// Selector 0: NumToString — converts D0.L to decimal Pascal string at A0.
// Selector 1: StringToNum — converts Pascal string at A0 to D0.L.
// PROCEDURE NumToString(theNumber: LONGINT; VAR theString: Str255);
// PROCEDURE StringToNum(theString: Str255; VAR theNumber: LONGINT);
// Inside Macintosh Volume I, I-489
// Pack7 (NumToString/StringToNum) ($A9EE): Selector 0: NumToString (D0→Str255 at A0), Selector 1: StringToNum (Str255 at A0→D0)
(true, 0x1EE) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp);
cpu.write_reg(Register::A7, sp + 2);
match selector {
0 => {
// NumToString: D0.L = number, A0 = pointer to Str255 result
let number = cpu.read_reg(Register::D0) as i32;
let a0 = cpu.read_reg(Register::A0);
bus.write_pstring(a0, format!("{}", number).as_bytes());
}
1 => {
// StringToNum: A0 = pointer to Pascal string, D0.L = result
let a0 = cpu.read_reg(Register::A0);
let bytes = bus.read_pstring(a0);
let s = String::from_utf8_lossy(&bytes);
let num: i32 = s.trim().parse().unwrap_or(0);
cpu.write_reg(Register::D0, num as u32);
}
_ => {
eprintln!("[TRAP] Pack7: unknown selector {}", selector);
}
}
Ok(())
}
// Pack12 ($A82E) — Color Picker Package
// Inside Macintosh Volume V, V-174..V-175.
// MPW Universal Interfaces 3.4 ColorPicker.h declares:
// Fix2SmallFract(Fixed) THREEWORDINLINE(0x3F3C, 0x0001, 0xA82E)
// SmallFract2Fix(SmallFract) THREEWORDINLINE(0x3F3C, 0x0002, 0xA82E)
// CMY2RGB(...) THREEWORDINLINE(0x3F3C, 0x0003, 0xA82E)
// RGB2CMY(...) THREEWORDINLINE(0x3F3C, 0x0004, 0xA82E)
// HSL2RGB(...) THREEWORDINLINE(0x3F3C, 0x0005, 0xA82E)
// GetColor(...) THREEWORDINLINE(0x3F3C, 0x0009, 0xA82E)
//
// SmallFract is documented as the low-order word of a Fixed
// number, so Fix2SmallFract drops the integer part while
// SmallFract2Fix zero-extends the fractional word into a
// 16.16 Fixed value with integer part 0.
(true, 0x02E) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp);
match selector {
// Fix2SmallFract (selector 1)
// FUNCTION Fix2SmallFract(f: Fixed): SmallFract;
// Stack: [result(2)] [f(4)] [sel(2)] — pop 6, leave 2
// Inside Macintosh Volume V, V-175
1 => {
let f = bus.read_long(sp + 2);
let small_fract = (f & 0xFFFF) as u16;
bus.write_word(sp + 6, small_fract);
cpu.write_reg(Register::A7, sp + 6);
}
// SmallFract2Fix (selector 2)
// FUNCTION SmallFract2Fix(s: SmallFract): Fixed;
// Stack: [result(4)] [s(2)] [sel(2)] — pop 4, leave 4
// Inside Macintosh Volume V, V-175
2 => {
let s = bus.read_word(sp + 2) as u32;
let fixed = s;
bus.write_long(sp + 4, fixed);
cpu.write_reg(Register::A7, sp + 4);
}
// CMY2RGB(3), RGB2CMY(4) — component-wise complements
// between the subtractive CMY and additive RGB models.
// PROCEDURE XXX(srcColor: XColor; VAR dstColor: YColor);
// Stack: [src_ptr(4)] [dst_ptr(4)] [sel(2)] — pop 10
// Inside Macintosh Volume V, V-175; Volume VI, 19-10
3 | 4 => {
let src_ptr = bus.read_long(sp + 6);
let dst_ptr = bus.read_long(sp + 2);
for i in 0..3u32 {
let component = bus.read_word(src_ptr + i * 2);
bus.write_word(dst_ptr + i * 2, !component);
}
cpu.write_reg(Register::A7, sp + 10);
}
// HSL2RGB(5)
// PROCEDURE HSL2RGB(hColor: HSLColor; VAR rColor: RGBColor);
// Stack: [src_ptr(4)] [dst_ptr(4)] [sel(2)] — pop 10
// Inside Macintosh Volume V, V-175;
// Volume VI, 19-10..19-13.
5 => {
fn hue_to_rgb(p: f64, q: f64, mut t: f64) -> f64 {
if t < 0.0 {
t += 1.0;
} else if t > 1.0 {
t -= 1.0;
}
if t < 1.0 / 6.0 {
return p + (q - p) * 6.0 * t;
}
if t < 1.0 / 2.0 {
return q;
}
if t < 2.0 / 3.0 {
return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
}
p
}
let src_ptr = bus.read_long(sp + 6);
let dst_ptr = bus.read_long(sp + 2);
let hue = bus.read_word(src_ptr) as f64 / 65535.0;
let saturation = bus.read_word(src_ptr + 2) as f64 / 65535.0;
let lightness = bus.read_word(src_ptr + 4) as f64 / 65535.0;
let (red, green, blue) = if saturation == 0.0 {
(lightness, lightness, lightness)
} else {
let q = if lightness < 0.5 {
lightness * (1.0 + saturation)
} else {
lightness + saturation - lightness * saturation
};
let p = 2.0 * lightness - q;
(
hue_to_rgb(p, q, hue + 1.0 / 3.0),
hue_to_rgb(p, q, hue),
hue_to_rgb(p, q, hue - 1.0 / 3.0),
)
};
let to_word = |component: f64| -> u16 {
(component.clamp(0.0, 1.0) * 65535.0).round() as u16
};
bus.write_word(dst_ptr, to_word(red));
bus.write_word(dst_ptr + 2, to_word(green));
bus.write_word(dst_ptr + 4, to_word(blue));
cpu.write_reg(Register::A7, sp + 10);
}
// RGB2HSL(6), HSV2RGB(7), RGB2HSV(8)
// are the remaining Color Picker conversions from
// IM:V V-175 and IM:VI 19-10..19-11.
6 => {
let src_ptr = bus.read_long(sp + 6);
let dst_ptr = bus.read_long(sp + 2);
let r = bus.read_word(src_ptr) as f64 / 65535.0;
let g = bus.read_word(src_ptr + 2) as f64 / 65535.0;
let b = bus.read_word(src_ptr + 4) as f64 / 65535.0;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
let lightness = (max + min) / 2.0;
let (hue, saturation) = if delta == 0.0 {
(0.0, 0.0)
} else {
let saturation = if lightness <= 0.5 {
delta / (max + min)
} else {
delta / (2.0 - max - min)
};
let mut hue = if max == r {
(g - b) / delta
} else if max == g {
2.0 + (b - r) / delta
} else {
4.0 + (r - g) / delta
};
if hue < 0.0 {
hue += 6.0;
}
(hue / 6.0, saturation)
};
let to_word = |component: f64| -> u16 {
(component.clamp(0.0, 1.0) * 65535.0).round() as u16
};
bus.write_word(dst_ptr, to_word(hue));
bus.write_word(dst_ptr + 2, to_word(saturation));
bus.write_word(dst_ptr + 4, to_word(lightness));
cpu.write_reg(Register::A7, sp + 10);
}
7 => {
let src_ptr = bus.read_long(sp + 6);
let dst_ptr = bus.read_long(sp + 2);
let hue = bus.read_word(src_ptr) as f64 / 65535.0;
let saturation = bus.read_word(src_ptr + 2) as f64 / 65535.0;
let value = bus.read_word(src_ptr + 4) as f64 / 65535.0;
let to_word = |component: f64| -> u16 {
(component.clamp(0.0, 1.0) * 65535.0).round() as u16
};
let (red, green, blue) = if saturation == 0.0 {
(value, value, value)
} else {
let h6 = hue * 6.0;
let sector = h6.floor() as i32;
let frac = h6 - sector as f64;
let p = value * (1.0 - saturation);
let q = value * (1.0 - saturation * frac);
let t = value * (1.0 - saturation * (1.0 - frac));
match sector.rem_euclid(6) {
0 => (value, t, p),
1 => (q, value, p),
2 => (p, value, t),
3 => (p, q, value),
4 => (t, p, value),
_ => (value, p, q),
}
};
bus.write_word(dst_ptr, to_word(red));
bus.write_word(dst_ptr + 2, to_word(green));
bus.write_word(dst_ptr + 4, to_word(blue));
cpu.write_reg(Register::A7, sp + 10);
}
8 => {
let src_ptr = bus.read_long(sp + 6);
let dst_ptr = bus.read_long(sp + 2);
let r = bus.read_word(src_ptr) as f64 / 65535.0;
let g = bus.read_word(src_ptr + 2) as f64 / 65535.0;
let b = bus.read_word(src_ptr + 4) as f64 / 65535.0;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
let value = max;
let saturation = if max == 0.0 { 0.0 } else { delta / max };
let hue = if delta == 0.0 {
0.0
} else {
let mut hue = if max == r {
(g - b) / delta
} else if max == g {
2.0 + (b - r) / delta
} else {
4.0 + (r - g) / delta
};
if hue < 0.0 {
hue += 6.0;
}
hue / 6.0
};
let to_word = |component: f64| -> u16 {
(component.clamp(0.0, 1.0) * 65535.0).round() as u16
};
bus.write_word(dst_ptr, to_word(hue));
bus.write_word(dst_ptr + 2, to_word(saturation));
bus.write_word(dst_ptr + 4, to_word(value));
cpu.write_reg(Register::A7, sp + 10);
}
// GetColor (selector 9)
// FUNCTION GetColor(where: Point; prompt: Str255;
// inColor: RGBColor; VAR outColor: RGBColor): BOOLEAN;
// Stack: [result(2)] [outColorPtr(4)] [inColorPtr(4)] [prompt(4)]
// [where(4)] [sel(2)] — pop 18, leave 2
// Inside Macintosh Volume V, V-174
9 => {
// Return FALSE (user cancelled)
bus.write_word(sp + 18, 0);
cpu.write_reg(Register::D0, 0);
cpu.write_reg(Register::A7, sp + 18);
}
_ => {
eprintln!("[PACK12] Unknown selector {} — popping 2 bytes", selector);
cpu.write_reg(Register::A7, sp + 2);
}
}
Ok(())
}
// ========== AliasDispatch ($A823) ==========
// AliasDispatch ($A823)
// Selector-based dispatcher for Alias Manager routines.
// Selector in D0.
// Inside Macintosh Volume VI, 9-17; Files 1992, 4-15
// AliasDispatch ($A823): Selector 0 = FindFolder (preferences-only stub returning Mac VFS dirID); other selectors return paramErr
(true, 0x023) => {
let sp = cpu.read_reg(Register::A7);
let selector = cpu.read_reg(Register::D0) & 0xFFFF;
match selector {
// FindFolder (selector $0000)
// FUNCTION FindFolder(vRefNum: INTEGER; folderType: OSType;
// createFolder: BOOLEAN; VAR foundVRefNum: INTEGER;
// VAR foundDirID: LONGINT): OSErr;
// Inside Macintosh Volume VI, 9-28
// Stack (rightmost on top):
// SP+0: foundDirID_ptr(4) SP+4: foundVRefNum_ptr(4)
// SP+8: createFolder(2) SP+10: folderType(4)
// SP+14: vRefNum(2) SP+16: result(2)
0 => {
let dirid_ptr = bus.read_long(sp);
let vref_ptr = bus.read_long(sp + 4);
let _create = bus.read_word(sp + 8) != 0;
let folder_type = bus.read_long(sp + 10);
let v_ref_num = bus.read_word(sp + 14) as i16;
let type_bytes = folder_type.to_be_bytes();
let type_str = std::str::from_utf8(&type_bytes).unwrap_or("????");
eprintln!(
"[ALIAS] FindFolder vRefNum={} type='{}' (${:08X})",
v_ref_num, type_str, folder_type
);
let found_dir_id = if folder_type == u32::from_be_bytes(*b"pref") {
self.ensure_vfs_directory("System Folder/Preferences")
} else {
2
};
bus.write_word(vref_ptr, (-1i16) as u16);
bus.write_long(dirid_ptr, found_dir_id);
// Pop 16 bytes params, leave 2-byte result
bus.write_word(sp + 16, 0); // noErr
cpu.write_reg(Register::A7, sp + 16);
}
// NewAlias (selector $0002)
// FUNCTION NewAlias(fromFile: FSSpecPtr; target: FSSpecPtr;
// VAR alias: AliasHandle): OSErr;
// Stack (rightmost on top):
// SP+0: alias_ptr(4) SP+4: target_ptr(4)
// SP+8: fromFile_ptr(4)
// SP+12: result(2)
// Inside Macintosh Volume VI, 9-57
0x0002 => {
let alias_ptr = bus.read_long(sp);
let target_ptr = bus.read_long(sp + 4);
let _from_file_ptr = bus.read_long(sp + 8);
let target_name = crate::trap::types::read_fsspec_name(bus, target_ptr);
eprintln!("[ALIAS] NewAlias target='{}'", target_name);
// Create a tiny placeholder alias handle so callers that check for
// a non-NULL handle continue to run.
if alias_ptr != 0 {
let alias_data = bus.alloc(16);
bus.write_word(alias_data, 16); // minimal length marker
let alias_handle = bus.alloc(4);
bus.write_long(alias_handle, alias_data);
bus.write_long(alias_ptr, alias_handle);
}
bus.write_word(sp + 12, 0); // noErr
cpu.write_reg(Register::A7, sp + 12);
}
// ResolveAliasFile (selector $000C)
// FUNCTION ResolveAliasFile(VAR theSpec: FSSpec;
// resolveAliasChains: Boolean;
// VAR targetIsFolder: Boolean;
// VAR wasAliased: Boolean): OSErr;
// Macintosh Toolbox Essentials 1992, 7-52
// Stack (rightmost on top):
// SP+0: wasAliased_ptr(4) SP+4: targetIsFolder_ptr(4)
// SP+8: resolveAliasChains(2) SP+10: theSpec_ptr(4)
// SP+14: result(2)
0x000C => {
let was_aliased_ptr = bus.read_long(sp);
let target_is_folder_ptr = bus.read_long(sp + 4);
let _resolve_chains = bus.read_word(sp + 8) != 0;
let spec_ptr = bus.read_long(sp + 10);
let name = crate::trap::types::read_fsspec_name(bus, spec_ptr);
eprintln!("[ALIAS] ResolveAliasFile spec='{}'", name);
// Not an alias, not a folder
bus.write_byte(was_aliased_ptr, 0);
bus.write_byte(target_is_folder_ptr, 0);
// Pop 14 bytes params, leave 2-byte result
bus.write_word(sp + 14, 0); // noErr
cpu.write_reg(Register::A7, sp + 14);
}
_ => {
eprintln!(
"[ALIAS] Unimplemented selector {} (${:04X})",
selector, selector
);
return Some(Err(Error::Halted));
}
}
Ok(())
}
// ========== CursorDeviceDispatch ($AADB) ==========
// Cursor Device Manager dispatcher. The classic glue passes
// selector $0B in D0; this path clears the Pascal result slot
// and pops the selector word so the caller sees noErr.
// CursorDeviceDispatch ($AADB): Returns 0
(true, 0x2DB) => {
let sp = cpu.read_reg(Register::A7);
bus.write_word(sp + 2, 0);
cpu.write_reg(Register::A7, sp + 2);
cpu.write_reg(Register::D0, 0);
Ok(())
}
// ========== Movie Toolbox Dispatch ($AAAA) ==========
// Inside Macintosh: QuickTime (1993), pp. 2-33, 2-82 to 2-84.
// Public MPW declarations:
// pascal OSErr EnterMovies(void);
// pascal void ExitMovies(void);
// Single trap dispatcher for the entire QuickTime Movie
// Toolbox API; the MPW glue loads the routine selector in
// D0 before executing `_AAAA` (selector 1 = EnterMovies).
// The zero-argument client calls exercised by the fixture
// are stack-neutral.
//
// A complete Movie Toolbox emulation is a substantial
// multi-iteration project (movies, tracks, media handlers,
// codecs). Until those land, the games we've identified as
// QuickTime-gated (Souls In The System) probe the Movie
// Toolbox during init via EnterMovies/ExitMovies but don't
// actually depend on movie playback for their menu→
// gameplay flow. A noErr-returning stub that pops the
// selector + declared param size lets those games proceed
// past init; titles that hit a real movie playback call
// can be addressed by upgrading specific selectors as
// they surface.
//
// Pre-fix the trap was UNIMPLEMENTED → halted the runner.
// Post-fix: return D0=0 (noErr), preserving the stack.
// EnterMovies also writes its OSErr result into the caller's
// result slot so the MPW wrapper sees the noErr value.
// Diagnostic logging still uses
// SYSTEMLESS_TRACE_QUICKTIME=1 for diagnostic visibility.
// Movie Toolbox Dispatch ($AAAA): D0 carries the selector;
// zero-arg EnterMovies/ExitMovies calls are stack-neutral,
// and EnterMovies writes its noErr result for the caller.
// Real Movie Toolbox emulation pending.
(true, 0x2AA) => {
let selector = cpu.read_reg(Register::D0) as u16;
if super::dispatch::trace_quicktime_enabled() {
static QT_LOG_COUNT: std::sync::atomic::AtomicU32 =
std::sync::atomic::AtomicU32::new(0);
if QT_LOG_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 20 {
eprintln!(
"[QUICKTIME] MovieToolboxDispatch selector_in_d0=${:04X}",
selector
);
}
}
if selector == 1 {
let sp = cpu.read_reg(Register::A7);
bus.write_word(sp, 0);
}
return_noerr(cpu)
}
// SetFractEnable ($A814)
// Enables or disables fractional character widths for the
// Font Manager's character-width tables.
// PROCEDURE SetFractEnable(fractEnable: BOOLEAN); [Not in ROM]
// Inside Macintosh Volume IV (1986), p. IV-32.
//
// Per IM:IV IV-32: "If fractEnable is TRUE, fractional
// character widths are enabled; if it's FALSE, the Font
// Manager uses integer widths. To ensure compatibility
// with existing applications, fractional character widths
// are disabled by default." The assembly-language note on
// the same page confirms: "From assembly language, you
// can change the value of the global variable FractEnable."
//
// The FractEnable low-memory global lives at $0BF4 per
// Macintosh Family Hardware Reference 2nd Ed. (1990)
// Appendix B and the MPW LowMem.h SystemGlobals table.
// MPW exposes the global via TWOWORDINLINE accessors:
// LMGetFractEnable() = MOVE.B $0BF4, D0 (0x1EB8 0x0BF4)
// LMSetFractEnable(v) = MOVE.B v, $0BF4 (0x11DF 0x0BF4)
//
// Pascal PROCEDURE protocol (caller perspective):
// Stack on entry: SP+0: fractEnable(2) — the BOOLEAN
// argument encoded as a 2-byte word with the value byte
// in the HIGH byte (MPW Pascal BOOLEAN convention).
// The trap pops 2 bytes; no function-result slot is
// reserved (this is a PROCEDURE, not a FUNCTION).
//
// Byte-write semantic: the real System 7.5 ROM writes the
// raw Pascal BOOLEAN high byte verbatim to $0BF4 — TRUE
// becomes the byte value 0x01 (not a normalised 0xFF) and
// FALSE becomes 0x00. Systemless mirrors this exact byte by
// reading SP+0 directly (no normalisation). BasiliskII
// System 7.5.3 ROM follows the same convention, witnessed
// by `a814_setfractenable_strict`.
//
// Regression coverage:
// tests::setfractenable_true_writes_one_byte_verbatim_to_fract_enable_global
// tests::setfractenable_false_writes_zero_byte_to_fract_enable_global
// tests::setfractenable_consumes_two_byte_boolean_argument_and_balances_stack
// SetFractEnable ($A814): Writes FractEnable low-mem global ($0BF4); per IM:IV IV-32
(true, 0x014) => {
let sp = cpu.read_reg(Register::A7);
// Pascal BOOLEAN at SP+0 (MPW convention: value byte
// in the high byte of the 2-byte stack slot).
let fract_enable_byte = bus.read_byte(sp);
cpu.write_reg(Register::A7, sp + 2);
bus.write_byte(0x0BF4, fract_enable_byte);
Ok(())
}
// ========== Printing Manager ==========
// PrGlue ($A8FD)
// Printing Manager dispatch. 32-bit selector on top of the stack.
// Inside Macintosh Volume V (1986), p. V-408.
//
// Selector format (assembly-language note, IM:V V-408):
// bits 31-24 = routine selector
// bits 23-16 = return value byte count
// bits 15-8 = parameter byte count (bytes to pop after selector)
//
// In emulation, printing is not supported. All routines are no-ops
// that pop their parameters and write default return values.
// PrSetError / PrError still model the shared PrintErr state so
// callers can observe the last Printing Manager result code.
//
// Regression coverage:
// tests::prglue_selector_param_byte_count_controls_stack_pop
// tests::prglue_propendoc_returns_nil_and_consumes_three_pointer_arguments
// tests::prglue_prvalidate_returns_false_boolean_result
// tests::prglue_prstldialog_returns_true_boolean_result
// tests::prglue_prjobdialog_returns_true_boolean_result
// tests::prglue_prclosedoc_consumes_tpprport_argument_without_function_result_slot
// tests::prglue_prerror_returns_noerr_word_with_zero_result_bits_selector
// tests::prglue_prseterror_consumes_ierr_word_argument_without_function_result_slot
// tests::prglue_prseterror_updates_prerror_state_roundtrip
// PrGlue ($A8FD): Selector-based Printing Manager dispatcher per
// IM:V V-408, with no-op HLE implementations for printing-disabled
// runtime paths.
(true, 0x0FD) => {
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_long(sp);
let routine = (selector >> 24) & 0xFF;
// Extract param size and result size from the selector.
// IM:V V-408 encodes return size in bits 23-16 and
// parameter size in bits 15-8.
let result_bytes = (selector >> 16) & 0xFF;
let param_bytes = (selector >> 8) & 0xFF;
let total_pop = 4 + param_bytes; // selector + params
match routine {
0x04 => {
// PrOpenDoc: returns TPPrPort (4 bytes) — return NIL
// Even when the selector's result-size bits are 0
// ($04000C00), callers reserve a TPPrPort result
// slot per routine signature. Mirror the nil return
// in D0 as well so inline shims can observe it.
self.printing_error = 0;
bus.write_long(sp + total_pop, 0);
cpu.write_reg(Register::D0, 0);
cpu.write_reg(Register::A7, sp + total_pop);
}
0x08 => {
// PrCloseDoc: consumes one TPPrPort argument and
// returns no function result.
self.printing_error = 0;
cpu.write_reg(Register::A7, sp + total_pop);
}
0xC8 | 0xD0 => {
// PrOpen / PrClose: procedures with no stack
// arguments. They consume only the selector long
// and do not perturb the shared PrintErr state.
cpu.write_reg(Register::A7, sp + total_pop);
}
0x2A | 0x32 => {
// PrStlDialog / PrJobDialog: returns BOOLEAN (2 bytes)
// Return TRUE (user clicked OK) so games proceed past print dialogs
self.printing_error = 0;
if result_bytes >= 2 {
bus.write_word(sp + total_pop, 1); // TRUE
}
cpu.write_reg(Register::D0, 1);
cpu.write_reg(Register::A7, sp + total_pop);
}
0x52 => {
// PrValidate: returns BOOLEAN (2 bytes)
// Return FALSE (record is valid, no changes needed)
self.printing_error = 0;
if result_bytes >= 2 {
bus.write_word(sp + total_pop, 0); // FALSE
}
cpu.write_reg(Register::D0, 0);
cpu.write_reg(Register::A7, sp + total_pop);
}
0xBA => {
// PrError: returns INTEGER (2 bytes). MPW encodes
// the selector as 0xBA00_0000 — the per-routine
// return-size bits are zero for PrError, but real
// ROM returns a 2-byte result regardless because
// the dispatcher knows the routine signature by
// trap table. Return the stored PrintErr word so
// PrSetError can affect later queries.
bus.write_word(sp + total_pop, self.printing_error as u16);
cpu.write_reg(Register::D0, self.printing_error as u32);
cpu.write_reg(Register::A7, sp + total_pop);
}
0xC0 => {
// PrSetError: stores the new printing error code in
// the shared PrintErr global and returns no result.
self.printing_error = bus.read_word(sp + 4) as i16;
cpu.write_reg(Register::D0, 0);
cpu.write_reg(Register::A7, sp + total_pop);
}
_ => {
// All other printing routines: pop params, write zero result
if result_bytes == 4 {
bus.write_long(sp + total_pop, 0);
} else if result_bytes == 2 {
bus.write_word(sp + total_pop, 0);
}
self.printing_error = 0;
cpu.write_reg(Register::A7, sp + total_pop);
}
}
Ok(())
}
// ========== Script Manager ==========
// ScriptUtil ($A8B5)
// Script Manager dispatch. Selector is a LONGINT on top of the stack.
// Inside Macintosh Volume V, V-288
//
// In the emulator we always return Roman script (0) for script queries
// and noErr for set operations. This is sufficient for English-only games.
//
// Regression coverage:
// fontscript_returns_roman_script
// intlscript_returns_roman_script
// keyscript_returns_roman_script
// font2script_returns_roman_script
// getenvirons_returns_zero
// setenvirons_returns_noerr
// getscript_returns_zero
// setscript_returns_noerr
// charbyte_returns_single_byte
// chartype_returns_zero
// char2pixel_returns_zero
// pixel2char_maps_pixel_to_character
// transliterate_converts_text
// findword_locates_word_boundaries
// hilitetext_computes_highlight_ranges
// drawjust_draws_justified_text
// measurejust_measures_justified_text
// ScriptUtil ($A8B5): Dispatches selectors 0-22 (FontScript, IntlScript, KeyScript, Font2Script, GetEnvirons, SetEnvirons, GetScript, SetScript, CharByte, CharType, Char2Pixel); returns smRoman/noErr/0; per IM:V V-288
(true, 0x0B5) => {
let sp = cpu.read_reg(Register::A7);
// MPW's inline wraps ScriptUtil selectors as
// MOVE.L #<encoding>.L, -(SP)
// _ScriptUtil
// where the high word encodes result-size and argument-count
// metadata. Older Script Manager calls use the low byte as the
// routine number; System 7 text utilities use full selectors.
let raw_selector = bus.read_long(sp);
let selector = (raw_selector & 0xFF) as i32;
match raw_selector {
// TruncString ($8208FFE0): FUNCTION TruncString(width: INTEGER;
// VAR theString: Str255; truncWhere: TruncCode): INTEGER
// Returning smNotTruncated leaves the string unchanged.
// Stack: selector(4), args(8), result(2). Pop selector + args.
// Inside Macintosh Volume VI, 14-59..14-60 and Table C-3.
0x8208_FFE0 => {
bus.write_word(sp + 12, 0); // smNotTruncated
cpu.write_reg(Register::A7, sp + 12);
return Some(Ok(()));
}
// TruncText ($820CFFDE): FUNCTION TruncText(width: INTEGER;
// textPtr: Ptr; VAR length: INTEGER; truncWhere: TruncCode): INTEGER
// Returning smNotTruncated leaves the pointed-to text and length unchanged.
// Stack: selector(4), args(12), result(2). Pop selector + args.
// Inside Macintosh Volume VI, 14-59..14-60 and Table C-3.
0x820C_FFDE => {
bus.write_word(sp + 16, 0); // smNotTruncated
cpu.write_reg(Register::A7, sp + 16);
return Some(Ok(()));
}
_ => {}
}
match selector {
// FontScript (0): FUNCTION FontScript: INTEGER
// Returns script code. Stack: selector(4), result space(2)
0 => {
bus.write_word(sp + 4, 0); // smRoman = 0
cpu.write_reg(Register::A7, sp + 4); // pop selector, result stays
}
// IntlScript (2): FUNCTION IntlScript: INTEGER
2 => {
bus.write_word(sp + 4, 0); // smRoman
cpu.write_reg(Register::A7, sp + 4);
}
// KeyScript (4): FUNCTION KeyScript: INTEGER
4 => {
bus.write_word(sp + 4, 0); // smRoman
cpu.write_reg(Register::A7, sp + 4);
}
// Font2Script (6): FUNCTION Font2Script(fontNum: INTEGER): INTEGER
// Stack: selector(4), fontNum(2), result(2)
6 => {
bus.write_word(sp + 6, 0); // smRoman
cpu.write_reg(Register::A7, sp + 6); // pop selector + fontNum
}
// GetEnvirons (8): FUNCTION GetEnvirons(verb: INTEGER): LongInt
// Stack: selector(4), verb(2), result(4)
8 => {
bus.write_long(sp + 6, 0); // return 0
cpu.write_reg(Register::A7, sp + 6); // pop selector + verb
}
// SetEnvirons (10): FUNCTION SetEnvirons(verb: INTEGER; param: LongInt): OSErr
// Stack: selector(4), verb(2), param(4), result(2)
10 => {
bus.write_word(sp + 10, 0); // noErr
cpu.write_reg(Register::A7, sp + 10); // pop selector + verb + param
}
// GetScript (12): FUNCTION GetScript(script: INTEGER; verb: INTEGER): LongInt
// Stack: selector(4), script(2), verb(2), result(4)
12 => {
bus.write_long(sp + 8, 0); // return 0
cpu.write_reg(Register::A7, sp + 8); // pop selector + script + verb
cpu.write_reg(Register::D0, 0);
}
// SetScript (14): FUNCTION SetScript(script: INTEGER; verb: INTEGER; param: LongInt): OSErr
// Stack: selector(4), script(2), verb(2), param(4), result(2)
14 => {
bus.write_word(sp + 12, 0); // noErr
cpu.write_reg(Register::A7, sp + 12);
cpu.write_reg(Register::D0, 0);
}
// CharByte (16): FUNCTION CharByte(textBuf: Ptr; textOffset: INTEGER): INTEGER
// Returns smSingleByte (0) for all chars in Roman script.
// Stack: selector(4), textBuf(4), textOffset(2), result(2)
16 => {
bus.write_word(sp + 10, 0); // smSingleByte = 0
cpu.write_reg(Register::A7, sp + 10);
}
// CharType (18): FUNCTION CharType(textBuf: Ptr; textOffset: INTEGER): INTEGER
// Stack: selector(4), textBuf(4), textOffset(2), result(2)
18 => {
bus.write_word(sp + 10, 0); // return 0 (left-to-right)
cpu.write_reg(Register::A7, sp + 10);
}
// Char2Pixel (22): FUNCTION Char2Pixel(textBuf: Ptr; textLen: INTEGER;
// slop: INTEGER; offset: INTEGER; direction: INTEGER): INTEGER
// Stack: selector(4), textBuf(4), textLen(2), slop(2), offset(2),
// direction(2), result(2)
22 => {
bus.write_word(sp + 16, 0); // return 0 pixel offset
cpu.write_reg(Register::A7, sp + 16);
}
// Pixel2Char (20): FUNCTION Pixel2Char(textBuf: Ptr; textLen: INTEGER;
// slop: INTEGER; pixelWidth: INTEGER; VAR leadingEdge: BOOLEAN): INTEGER
// Pascal pushes args left-to-right (first arg deepest), so the
// VAR leadingEdge pointer is the LAST arg pushed and lives at
// sp+4 (just past the selector long). Layout post-trap-entry:
// sp+0 selector long
// sp+4 leadingEdge_ptr (last arg, 4 bytes)
// sp+8 pixelWidth (2 bytes)
// sp+10 slop (2 bytes)
// sp+12 textLen (2 bytes)
// sp+14 textBuf (first arg, 4 bytes)
// sp+18 INTEGER result slot
// Pop 18 bytes (selector + 14 arg bytes), leave 2-byte result.
// Inside Macintosh Volume V, V-310
20 => {
let leading_edge_ptr = bus.read_long(sp + 4);
if leading_edge_ptr != 0 {
bus.write_byte(leading_edge_ptr, 0);
}
bus.write_word(sp + 18, 0); // return offset 0
cpu.write_reg(Register::A7, sp + 18);
}
// Transliterate (24): FUNCTION Transliterate(srcHandle, dstHandle: Handle;
// target: INTEGER; srcMask: LongInt): OSErr
// Stack: selector(4), srcHandle(4), dstHandle(4), target(2),
// srcMask(4), result(2). Pop 14 bytes of args, leave result.
// Inside Macintosh Volume V, V-312
24 => {
bus.write_word(sp + 18, 0); // noErr
cpu.write_reg(Register::A7, sp + 18);
}
// FindWord (26): PROCEDURE FindWord(textPtr: Ptr; textLength, offset: INTEGER;
// leadingEdge: BOOLEAN; breaksPtr: Ptr; VAR offsets: OffsetTable)
// Pascal pushes args left-to-right (first arg deepest), so the
// VAR offsets pointer is the LAST arg pushed and lives at sp+4.
// Layout post-trap-entry:
// sp+0 selector long
// sp+4 offsets_ptr (last arg, 4 bytes)
// sp+8 breaksPtr (4 bytes)
// sp+12 leadingEdge (2 bytes)
// sp+14 offset (2 bytes)
// sp+16 textLength (2 bytes)
// sp+18 textPtr (first arg, 4 bytes)
// No return value. Pop 22 (selector + 18 arg bytes).
// OffsetTable is ARRAY[0..2] OF OffPair = 3*4 = 12 bytes
// (Inside Macintosh Volume VI, p. 33514 summary; Text 1993,
// p. 10664).
// Inside Macintosh Volume V, V-313
26 => {
let offsets_ptr = bus.read_long(sp + 4);
if offsets_ptr != 0 {
bus.write_bytes(offsets_ptr, &[0u8; 12]);
}
cpu.write_reg(Register::A7, sp + 22);
}
// HiliteText (28): PROCEDURE HiliteText(textPtr: Ptr; textLength,
// firstOffset, secondOffset: INTEGER; VAR offsets: OffsetTable)
// Pascal pushes args left-to-right; the VAR offsets pointer is
// the LAST arg pushed and lives at sp+4. Layout post-trap-entry:
// sp+0 selector long
// sp+4 offsets_ptr (last arg, 4 bytes)
// sp+8 secondOffset (2 bytes)
// sp+10 firstOffset (2 bytes)
// sp+12 textLength (2 bytes)
// sp+14 textPtr (first arg, 4 bytes)
// No return. Pop 18 (selector + 14 arg bytes). OffsetTable is
// 12 bytes (Inside Macintosh Volume VI, p. 33514 summary).
// Inside Macintosh Volume V, V-314
28 => {
let offsets_ptr = bus.read_long(sp + 4);
if offsets_ptr != 0 {
bus.write_bytes(offsets_ptr, &[0u8; 12]);
}
cpu.write_reg(Register::A7, sp + 18);
}
// DrawJust (30): PROCEDURE DrawJust(textPtr: Ptr; textLength, slop: INTEGER)
// No return, no output. Systemless does not implement justified text drawing
// here. Stack: selector(4) + 8 bytes of args; pop 12.
// Inside Macintosh Volume V, V-315
30 => {
cpu.write_reg(Register::A7, sp + 12);
}
// MeasureJust (32): PROCEDURE MeasureJust(textPtr: Ptr; textLength,
// slop: INTEGER; charLocs: Ptr)
// No return. Stack: selector(4) + 12 bytes of args; pop 16.
// Inside Macintosh Volume V, V-315
32 => {
cpu.write_reg(Register::A7, sp + 16);
}
_ => {
let result_bytes = (raw_selector >> 24) & 0x7F;
let arg_bytes = (raw_selector >> 16) & 0xFF;
if (raw_selector & 0x8000_0000) != 0
&& matches!(result_bytes, 0 | 1 | 2 | 4)
{
let result_sp = sp + 4 + arg_bytes;
match result_bytes {
1 => bus.write_byte(result_sp, 0),
2 => bus.write_word(result_sp, 0),
4 => bus.write_long(result_sp, 0),
_ => {}
}
eprintln!(
"[TRAP] ScriptUtil: unhandled encoded selector ${:08X}; popped {} arg bytes",
raw_selector, arg_bytes
);
cpu.write_reg(Register::A7, result_sp);
} else {
// Unknown legacy selector — pop the selector and return.
eprintln!("[TRAP] ScriptUtil: unhandled selector {}", selector);
cpu.write_reg(Register::A7, sp + 4);
}
}
}
Ok(())
}
// Pack9 ($A82B) — StackSpace alias
// Inside Macintosh Volume IV (1986), pp. IV-78 and IV-81;
// Inside Macintosh: Memory 1992, pp. 2-69 to 2-70.
// Pack9 maps directly to _StackSpace ($A065), which returns
// its LongInt result in D0 and consumes no Pascal arguments.
(true, 0x02B) => return self.dispatch_memory(false, 0x65, cpu, bus),
// Pack10 ($A82C) — NewEmptyHandle alias
// Inside Macintosh Volume IV (1986), pp. IV-78 and IV-81;
// Inside Macintosh: Memory 1992, p. 2-33.
// Pack10 maps directly to _NewEmptyHandle ($A066), which
// returns its Handle result in A0 and consumes no Pascal
// arguments.
(true, 0x02C) => return self.dispatch_memory(false, 0x66, cpu, bus),
// Pack11 ($A82D) — Edition Manager
// Inside Macintosh: Interapplication Communication 1993,
// pp. 12-60 and 12-103. `_Pack11` is the package trap for
// the Edition Manager; `InitEditionPack` is selector $0100
// and takes no Pascal arguments. The public MPW glue passes
// the selector in D0, matching the Pack13-style package trap
// convention.
//
// Keep the explicit `InitEditionPack` path separate from the
// generic selector-byte heuristic so the documented bootstrap
// routine remains obvious to future readers.
//
// Regression coverage:
// a82d_pack11_initeditionpack_strict
// Pack11 ($A82D): `_Pack11` Edition Manager. `InitEditionPack` selector $0100 returns noErr and takes no Pascal args; other selectors still use the generic pop fallback.
(true, 0x02D) => {
let selector = cpu.read_reg(Register::D0) as u16;
if selector == 0x0100 {
cpu.write_reg(Register::D0, 0);
} else {
let sp = cpu.read_reg(Register::A7);
let param_size = ((selector >> 8) & 0xFF) as u32;
let total = 2 + if (2..=48).contains(¶m_size) {
param_size
} else {
0
};
cpu.write_reg(Register::A7, sp + total);
cpu.write_reg(Register::D0, 0);
}
Ok(())
}
// Pack13 ($A82F) — Data Access Manager
// Inside Macintosh: Interapplication Communication 1993,
// pp. 12-60 and 12-103.
//
// The Data Access Manager macros place the routine selector
// in D0 and call _Pack13. Systemless HLE currently only needs
// the InitDBPack selector ($0100), so this arm is a D0-driven
// no-op: return noErr and preserve the caller's Pascal stack.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::pack13_initdbpack_selector_returns_noerr_and_preserves_stack
(true, 0x02F) => {
let _selector = cpu.read_reg(Register::D0) as u16;
cpu.write_reg(Register::D0, 0);
Ok(())
}
// Pack14 ($A830) — Help Manager
//
// Twenty-one-routine selector dispatcher providing balloon
// help: status query, balloon show/remove, font config,
// help-resource lookup, and help-message extraction.
//
// Selector encoding is `(arg_words << 8) | routine` per
// IM:VI 11148+ Pack14 trap macro table — the same
// Apple-Events Pack8 convention, NOT the Pack2/3/6 pure-
// low-byte convention. The high byte of the selector
// gives the number of WORDS of args pushed on the stack;
// arg_bytes = high_byte * 2. Total pop = 2 (selector) +
// arg_bytes.
//
// Pascal calling convention: caller pre-pushes a 2-byte
// result slot for OSErr / Boolean / Integer FUNCTION
// returns, then pushes args left-to-right (first source-
// listed arg deepest, last shallowest at SP+2 just above
// the selector word at SP+0). Trap pops 2 + arg_bytes,
// exposing the result slot at the new SP+0. We mirror
// each result to BOTH the stack slot AND D0 for callers
// that read either way (matches the Pack6 / IUMagString
// pattern).
//
// HLE compromise: Systemless has no Balloon Help subsystem
// — no cursor tracking, no balloon WDEF / window, no
// 'hmnu' / 'hdlg' / 'hrct' / 'hwin' / 'hovr' / 'hfdr'
// resource walking, no help font cache. Status queries
// collapse to "help disabled" (HMGetBalloons returns
// FALSE, HMIsBalloon returns FALSE); show/remove balloon
// ops return hmHelpDisabled (-850); set ops are no-op
// noErr; get ops write defensive defaults (NIL handles,
// 0 fonts, empty Rects, -1 resource IDs) plus the IM-
// documented error codes for "no resource set" paths
// (resNotFound for HMGet*ResID, hmHelpManagerNotInited
// for HMGetHelpMenuHandle); resource extraction routines
// return resNotFound. Apps that defensively check OSErr
// before dereffing fall through cleanly; apps that need
// actual help balloons see the documented "help disabled"
// path which matches what System 7.5.3 would do if the
// user toggled Balloon Help off via the Help menu.
//
// The selector encoding pinned by the existing stub
// heuristic was actively wrong: it interpreted the high
// byte as BYTE count rather than WORD count, and clamped
// to 2..=48. So $0104 HMSetBalloons (high=$01, true args
// = 2 bytes) popped only 2 (clamp filtered $01 out),
// leaving 2 args bytes + result slot stranded; $0B01
// HMShowBalloon (high=$0B = 11 words = 22 args bytes)
// popped 11 instead of 22, leaving 11 args bytes
// stranded. Any real-game caller would crash on RTS.
//
// Inside Macintosh: More Macintosh Toolbox 1993, ch. 3,
// Help Manager, pages 3-1..3-173 + selector summary at
// page 3-173 (MMTb 11320..11340).
// Pack14 / Help Manager ($A830): Per-selector Pascal frames per IM:MMTb 1993 ch.3 + selector table 3-173: $0002 HMRemoveBalloon pop 2 D0=hmHelpDisabled, $0003 HMGetBalloons pop 2 D0=0 result=FALSE, $0007 HMIsBalloon pop 2 D0=0 result=FALSE, $0104 HMSetBalloons pop 4 D0=0, $0108 HMSetFont pop 4 D0=0, $0109 HMSetFontSize pop 4 D0=0, $010C HMSetDialogResID pop 4 D0=0, $0200 HMGetHelpMenuHandle pop 6 writes NIL to *mh D0=hmHelpManagerNotInited, $020A HMGetFont pop 6 writes 0 to *font D0=0, $020B HMGetFontSize pop 6 writes 0 to *fontSize D0=0, $020D HMSetMenuResID pop 6 D0=0, $0213 HMGetDialogResID pop 6 writes -1 to *resID D0=resNotFound, $0215 HMGetBalloonWindow pop 6 writes NIL to *window D0=0, $0314 HMGetMenuResID pop 8 writes -1 to *resID D0=resNotFound, $040E HMBalloonRect pop 10 writes Rect(0,0,0,0) D0=0, $040F HMBalloonPict pop 10 writes NIL to *coolPict D0=0, $0410 HMScanTemplateItems pop 10 D0=resNotFound, $0711 HMExtractHelpMsg pop 16 D0=resNotFound, $0B01 HMShowBalloon pop 24 D0=hmHelpDisabled, $0E05 HMShowMenuBalloon pop 30 D0=hmHelpDisabled, $1306 HMGetIndHelpMsg pop 40 D0=resNotFound.
(true, 0x030) => {
const HM_HELP_DISABLED: i16 = -850;
const HM_HELP_MGR_NOT_INITED: i16 = -855;
const RES_NOT_FOUND: i16 = -192;
let sp = cpu.read_reg(Register::A7);
let selector = bus.read_word(sp);
// Helper: write OSErr/Integer result word to BOTH the
// stack slot at sp+pop_total AND D0, then advance A7.
let finish =
|bus: &mut MacMemoryBus, cpu: &mut dyn CpuOps, pop_total: u32, result: i16| {
bus.write_word(sp + pop_total, result as u16);
cpu.write_reg(Register::A7, sp + pop_total);
cpu.write_reg(Register::D0, result as i32 as u32);
};
match selector {
// FUNCTION HMRemoveBalloon: OSErr;
// IM:MMTb 3-105. Pop = 2 (selector only).
// No balloon ever up in HLE → noErr per the IM
// result table ("No error or the help balloon
// was removed").
0x0002 => finish(bus, cpu, 2, 0),
// FUNCTION HMGetBalloons: Boolean;
// IM:MMTb 3-98. Pop = 2.
// Help disabled in HLE → FALSE (0).
0x0003 => finish(bus, cpu, 2, 0),
// FUNCTION HMIsBalloon: Boolean;
// IM:MMTb 3-99. Pop = 2.
// No balloon up in HLE → FALSE (0).
0x0007 => finish(bus, cpu, 2, 0),
// FUNCTION HMSetBalloons(flag: Boolean): OSErr;
// IM:MMTb 3-107. Pop = 4 (selector + flag).
// Accept and ignore — no help to enable/disable.
0x0104 => finish(bus, cpu, 4, 0),
// FUNCTION HMSetFont(font: Integer): OSErr;
// IM:MMTb 3-112. Pop = 4. Accept and ignore.
0x0108 => finish(bus, cpu, 4, 0),
// FUNCTION HMSetFontSize(fontSize: Integer): OSErr;
// IM:MMTb 3-113. Pop = 4. Accept and ignore.
0x0109 => finish(bus, cpu, 4, 0),
// FUNCTION HMSetDialogResID(resID: Integer): OSErr;
// IM:MMTb 3-117. Pop = 4. Accept and ignore.
0x010C => finish(bus, cpu, 4, 0),
// FUNCTION HMGetHelpMenuHandle(VAR mh: MenuHandle): OSErr;
// IM:MMTb 3-109. Pop = 6 (sel + mh ptr).
// mh ptr at SP+2. Write NIL to *mh per
// hmHelpManagerNotInited contract.
0x0200 => {
let mh_ptr = bus.read_long(sp + 2);
if mh_ptr != 0 {
bus.write_long(mh_ptr, 0);
}
finish(bus, cpu, 6, HM_HELP_MGR_NOT_INITED);
}
// FUNCTION HMGetFont(VAR font: Integer): OSErr;
// IM:MMTb 3-110. Pop = 6. Write 0 (system font)
// to *font.
0x020A => {
let font_ptr = bus.read_long(sp + 2);
if font_ptr != 0 {
bus.write_word(font_ptr, 0);
}
finish(bus, cpu, 6, 0);
}
// FUNCTION HMGetFontSize(VAR fontSize: Integer): OSErr;
// IM:MMTb 3-111. Pop = 6. Write 0 (system size)
// to *fontSize.
0x020B => {
let size_ptr = bus.read_long(sp + 2);
if size_ptr != 0 {
bus.write_word(size_ptr, 0);
}
finish(bus, cpu, 6, 0);
}
// FUNCTION HMSetMenuResID(menuID, resID: Integer): OSErr;
// IM:MMTb 3-114. Pop = 6. resID at SP+2 (last
// arg), menuID at SP+4. Accept and ignore.
0x020D => finish(bus, cpu, 6, 0),
// FUNCTION HMGetDialogResID(VAR resID: Integer): OSErr;
// IM:MMTb 3-118. Pop = 6. Write -1 to *resID
// per "no hdlg set" → resNotFound contract.
0x0213 => {
let res_id_ptr = bus.read_long(sp + 2);
if res_id_ptr != 0 {
bus.write_word(res_id_ptr, (-1i16) as u16);
}
finish(bus, cpu, 6, RES_NOT_FOUND);
}
// FUNCTION HMGetBalloonWindow(VAR window: WindowPtr): OSErr;
// IM:MMTb 3-121. Pop = 6. Write NIL to *window
// per "no balloon up" contract.
0x0215 => {
let window_ptr = bus.read_long(sp + 2);
if window_ptr != 0 {
bus.write_long(window_ptr, 0);
}
finish(bus, cpu, 6, 0);
}
// FUNCTION HMGetMenuResID(menuID: Integer;
// VAR resID: Integer): OSErr;
// IM:MMTb 3-115. Pop = 8. resID ptr at SP+2
// (last arg), menuID at SP+6. Write -1 to
// *resID per "no hmnu set" → resNotFound.
0x0314 => {
let res_id_ptr = bus.read_long(sp + 2);
if res_id_ptr != 0 {
bus.write_word(res_id_ptr, (-1i16) as u16);
}
finish(bus, cpu, 8, RES_NOT_FOUND);
}
// FUNCTION HMBalloonRect(aHelpMsg: HMMessageRecord;
// VAR coolRect: Rect): OSErr;
// IM:MMTb 3-119. Pop = 10. coolRect ptr at SP+2
// (last arg), aHelpMsg ptr at SP+6. Write
// Rect(0,0,0,0) — empty, no balloon to size.
0x040E => {
let rect_ptr = bus.read_long(sp + 2);
if rect_ptr != 0 {
bus.write_word(rect_ptr, 0);
bus.write_word(rect_ptr + 2, 0);
bus.write_word(rect_ptr + 4, 0);
bus.write_word(rect_ptr + 6, 0);
}
finish(bus, cpu, 10, 0);
}
// FUNCTION HMBalloonPict(aHelpMsg: HMMessageRecord;
// VAR coolPict: PicHandle): OSErr;
// IM:MMTb 3-120. Pop = 10. coolPict ptr at SP+2.
// Write NIL to *coolPict.
0x040F => {
let pict_ptr = bus.read_long(sp + 2);
if pict_ptr != 0 {
bus.write_long(pict_ptr, 0);
}
finish(bus, cpu, 10, 0);
}
// FUNCTION HMScanTemplateItems(whichID,
// whichResFile: Integer;
// whichType: ResType): OSErr;
// IM:MMTb 3-116. Pop = 10. No help resources
// ever loaded → resNotFound.
0x0410 => finish(bus, cpu, 10, RES_NOT_FOUND),
// FUNCTION HMExtractHelpMsg(whichType: ResType;
// whichResID, whichMsg,
// whichState: Integer;
// VAR aHelpMsg:
// HMMessageRecord): OSErr;
// IM:MMTb 3-126. Pop = 16. No help resources →
// resNotFound. Don't touch aHelpMsg (caller's
// record stays untouched).
0x0711 => finish(bus, cpu, 16, RES_NOT_FOUND),
// FUNCTION HMShowBalloon(aHelpMsg: HMMessageRecord;
// tip: Point;
// alternateRect: RectPtr;
// tipProc: Ptr;
// theProc, variant,
// method: Integer): OSErr;
// IM:MMTb 3-100. Pop = 24. Help disabled →
// hmHelpDisabled.
0x0B01 => finish(bus, cpu, 24, HM_HELP_DISABLED),
// FUNCTION HMShowMenuBalloon(itemNum,
// itemMenuID: Integer;
// itemFlags,
// itemReserved: LongInt;
// tip: Point;
// alternateRect: RectPtr;
// tipProc: Ptr;
// theProc,
// variant: Integer): OSErr;
// IM:MMTb 3-103. Pop = 30. Help disabled →
// hmHelpDisabled.
0x0E05 => finish(bus, cpu, 30, HM_HELP_DISABLED),
// FUNCTION HMGetIndHelpMsg(whichType: ResType;
// whichResID, whichMsg,
// whichState: Integer;
// VAR options: LongInt;
// VAR tip: Point;
// VAR altRect: Rect;
// VAR theProc: Integer;
// VAR variant: Integer;
// VAR aHelpMsg:
// HMMessageRecord;
// VAR count: Integer): OSErr;
// IM:MMTb 3-128. Pop = 40. No help resources →
// resNotFound. Don't touch any VAR-out param —
// caller's records stay untouched per the IM
// contract that resNotFound means "did not
// populate anything".
0x1306 => finish(bus, cpu, 40, RES_NOT_FOUND),
// Unknown selector — pop just the 2-byte selector
// and leave the FUNCTION result slot untouched +
// D0 = noErr. A future System addition that
// assigns a new Pack14 routine should fill in a
// new arm above; the unknown-selector path is
// intentionally a permissive no-op so the caller
// can degrade gracefully.
_ => {
cpu.write_reg(Register::A7, sp + 2);
cpu.write_reg(Register::D0, 0);
}
}
Ok(())
}
// Pack15 ($A831) — Picture Utilities Package
//
// Seven-routine selector dispatcher providing image-
// metadata extraction (color counts, palettes, font
// names, comment IDs, source rectangles) from PICT and
// PixMap structures.
//
// Selector encoding is `(arg_words << 8) | routine` per
// IM:VI 18-18 selector summary — same Apple-Events Pack8
// / Pack14 convention. arg_bytes = high_byte * 2; total
// pop = 2 (selector) + arg_bytes.
//
// Pascal calling convention: caller pre-pushes a 2-byte
// OSErr result in D0, then args left-to-right (first
// source-listed arg deepest, last shallowest at SP+2).
// Pack15's public MPW glue loads the selector into D0
// (`MOVE.W #selector, D0`) and leaves only the Pascal
// arguments on the stack. The trap pops args only and
// returns the OSErr in D0; unlike Pack6 / Pack14, the
// caller-visible stack does not carry a separate result
// slot.
//
// HLE compromise: Systemless does not parse PICT opcodes
// for metadata extraction (no quantization, no font-
// name discovery, no comment-ID enumeration). The
// routines still return the documented OSErr surface and
// write defensive defaults to VAR-out parameters:
// NewPictInfo writes a unique, nonzero PictInfoID to
// *PictInfoID and registers it as live; RetrievePictInfo /
// GetPictInfo / GetPixMapInfo zero-fill the 104-byte
// PictInfo record (uniqueColors=0, depth=0, all
// counts=0, NIL palette/colorTable/font/comment
// handles, sourceRect=(0,0,0,0)) when the ID is live;
// RecordPictInfo / RecordPixMapInfo reject stale IDs
// with pictInfoIDErr (-11001); DisposPictInfo is
// idempotent and returns noErr on repeated calls. Apps
// that walk PictInfo for thumbnail / font discovery see
// "this picture has no metadata" — which is technically
// correct for our HLE since we don't model picture
// introspection. Apps that defensively check OSErr
// proceed cleanly.
//
// The previous heuristic at toolbox.rs:5957..5962
// interpreted the high byte as BYTE count not WORD
// count and clamped to (2..=48): RecordPictInfo $0403
// (high $04 = 4 words = 8 byte args) popped 6 instead
// of 10; GetPictInfo $0800 (high $08 = 8 words = 16
// byte args) popped 10 instead of 18. Every selector
// had wrong pop discipline. A real-game caller would
// have left 4..8 garbage arg bytes on the stack and
// crashed on the next RTS.
//
// Inside Macintosh Volume VI (1991), ch. 18, Picture
// Utilities Package, pages 18-1..18-18 + selector
// summary table at 18-18 (IM:VI 37669..37685).
// Pack15 / Picture Utilities ($A831): Per-selector Pascal frames per IM:VI 18-18 and Imaging With QuickDraw 1994 pp. 7-53..7-59: selector in D0, no selector word on stack. $0206 DisposPictInfo pop 4 D0=0, $0403 RecordPictInfo pop 8 D0=0, $0404 RecordPixMapInfo pop 8 D0=0, $0505 RetrievePictInfo pop 10 *PictInfo zeroed (104 bytes) D0=0, $0602 NewPictInfo pop 12 *PictInfoID=unique nonzero ID D0=0, $0800 GetPictInfo pop 16 *PictInfo zeroed (104 bytes) D0=0, $0801 GetPixMapInfo pop 16 *PictInfo zeroed (104 bytes) D0=0.
(true, 0x031) => {
// Size of PictInfo record per IM:VI 18-5:
// version(2) + uniqueColors(4) + thePalette(4) +
// theColorTable(4) + hRes(4) + vRes(4) + depth(2) +
// sourceRect(8) + 18 LongInts (textCount through
// reserved2) = 104 bytes.
const PICT_INFO_SIZE: u32 = 104;
let sp = cpu.read_reg(Register::A7);
let selector = cpu.read_reg(Register::D0) as u16;
let pop_total = ((selector >> 8) as u32) * 2;
// Helper: advance A7 by the packed-argument bytes and
// write the OSErr result to D0 only. Pack15's caller
// stack does not reserve a separate result slot.
let finish = |cpu: &mut dyn CpuOps, pop_total: u32, result: i16| {
cpu.write_reg(Register::A7, sp + pop_total);
cpu.write_reg(Register::D0, result as i32 as u32);
};
// Helper: zero-fill a PictInfo record at the given
// ptr. NIL ptr is a graceful no-op.
let zero_pict_info = |bus: &mut MacMemoryBus, ptr: u32| {
if ptr == 0 {
return;
}
for off in 0..PICT_INFO_SIZE {
bus.write_byte(ptr + off, 0);
}
};
match selector {
// FUNCTION DisposPictInfo(thePictInfoID:
// PictInfoID): OSErr;
// IM:VI 18-14. Pop = 4. Stack: PictInfoID(4).
// The live-ID registry is pruned on the first
// dispose, but BasiliskII treats repeated calls
// as noErr no-ops.
0x0206 => {
let pict_info_id = bus.read_long(sp);
let _ = self.pict_info_ids.remove(&pict_info_id);
finish(cpu, 4, 0);
}
// FUNCTION RecordPictInfo(thePictInfoID: PictInfoID;
// thePictHandle: PicHandle): OSErr;
// IM:VI 18-12. Pop = 8. Stack: PicHandle(4 last)
// + PictInfoID(4 first).
// Invalid IDs return pictInfoIDErr.
0x0403 => {
let pict_info_id = bus.read_long(sp + 4);
let result = if self.pict_info_ids.contains(&pict_info_id) {
0
} else {
-11001
};
finish(cpu, 8, result);
}
// FUNCTION RecordPixMapInfo(thePictInfoID: PictInfoID;
// thePixMapHandle: PixMapHandle): OSErr;
// IM:VI 18-12. Pop = 8. Same shape as
// RecordPictInfo. Invalid IDs return pictInfoIDErr.
0x0404 => {
let pict_info_id = bus.read_long(sp + 4);
let result = if self.pict_info_ids.contains(&pict_info_id) {
0
} else {
-11001
};
finish(cpu, 8, result);
}
// FUNCTION RetrievePictInfo(thePictInfoID:
// PictInfoID;
// VAR thePictInfo: PictInfo;
// colorsRequested: Integer): OSErr;
// IM:VI 18-13. Pop = 10. Stack: colorsRequested(2
// last) + thePictInfo ptr(4) + PictInfoID(4
// first). Invalid IDs return pictInfoIDErr; live
// IDs zero-fill the PictInfo record.
0x0505 => {
let pict_info_id = bus.read_long(sp + 6);
if self.pict_info_ids.contains(&pict_info_id) {
let info_ptr = bus.read_long(sp + 2);
zero_pict_info(bus, info_ptr);
finish(cpu, 10, 0);
} else {
finish(cpu, 10, -11001);
}
}
// FUNCTION NewPictInfo(VAR thePictInfoID:
// PictInfoID; verb: Integer;
// colorsRequested: Integer;
// colorPickMethod: Integer;
// version: Integer): OSErr;
// IM:VI 18-11. Pop = 12. Stack: version(2 last)
// + colorPickMethod(2) + colorsRequested(2) +
// verb(2) + PictInfoID ptr(4 first). Mint a
// unique nonzero
// PictInfoID and write it to *PictInfoID.
0x0602 => {
let id_ptr = bus.read_long(sp + 8);
if id_ptr != 0 {
static PICT_INFO_ID_COUNTER: std::sync::atomic::AtomicU32 =
std::sync::atomic::AtomicU32::new(1);
let pict_info_id = PICT_INFO_ID_COUNTER
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
bus.write_long(id_ptr, pict_info_id);
self.pict_info_ids.insert(pict_info_id);
}
finish(cpu, 12, 0);
}
// FUNCTION GetPictInfo(thePictHandle: PicHandle;
// VAR thePictInfo: PictInfo;
// verb: Integer;
// colorsRequested: Integer;
// colorPickMethod: Integer;
// version: Integer): OSErr;
// IM:VI 18-9. Pop = 16. Stack: version(2 last)
// + colorPickMethod(2) + colorsRequested(2) +
// verb(2) + thePictInfo ptr(4) + PicHandle(4
// first). Zero-fill the 104-byte PictInfo record.
0x0800 => {
let info_ptr = bus.read_long(sp + 8);
zero_pict_info(bus, info_ptr);
finish(cpu, 16, 0);
}
// FUNCTION GetPixMapInfo(thePixMapHandle:
// PixMapHandle;
// VAR thePictInfo: PictInfo;
// verb: Integer;
// colorsRequested: Integer;
// colorPickMethod: Integer;
// version: Integer): OSErr;
// IM:VI 18-10. Pop = 16. Same shape as
// GetPictInfo with PixMapHandle in place of
// PicHandle. Zero-fill PictInfo record.
0x0801 => {
let info_ptr = bus.read_long(sp + 8);
zero_pict_info(bus, info_ptr);
finish(cpu, 16, 0);
}
// Unknown selector — pop the encoded argument
// bytes, leave the FUNCTION result slot
// untouched, and return noErr in D0. A future
// System addition with a new Pack15 routine
// should add a new arm above.
_ => {
cpu.write_reg(Register::A7, sp + pop_total);
cpu.write_reg(Register::D0, 0);
}
}
Ok(())
}
// ========================================================
// System 7+ Dispatch Managers (no-op family — 10 traps)
// ========================================================
//
// Ten selector-based dispatchers for System-7-and-later
// subsystems whose underlying machinery Systemless does not
// model: Component Manager (1991), Object Support Library,
// Dictionary Manager, Text Services Manager, Docking
// Manager, Mixed Mode Manager (PowerPC), Code Fragment
// Manager (PowerPC), Icon Utilities, Thread Manager, and
// Translation Manager. Each one routes a sub-routine call
// selected by either a stack-pushed selector word
// (Component Manager) or a routine number in D0
// (everything else, Pack8/Pack14 convention).
//
// HLE compromise — load-bearing rationale:
// 1. Apps written for System 7+ uniformly probe presence
// via the documented Gestalt selector before calling
// the dispatcher. The standard idiom is:
// if Gestalt('cfrg', &response) = noErr
// and BTst(response, gestaltCFMPresent)
// then ... call CodeFragmentManager routines ...
// else ... fall back to non-CFM path ...
// Systemless's Gestalt arm at src/trap/toolbox.rs:1500+
// returns gestaltUndefSelectorErr (-5551) for every
// selector listed below, so well-behaved apps see
// "absent" and skip the dispatcher entirely.
// 2. Most of the cluster still follows the stub/no-op
// pattern: if a future corpus title bypasses
// Gestalt and calls one of those dispatchers
// directly, we can enumerate the selectors and
// promote that arm to Partial in the Pack14 style
// (see Help Manager $A830 at src/trap/toolbox.rs:5666
// for the canonical `(arg_words << 8) | routine`
// enumeration shape).
// 3. ThreadDispatch is the exception: the public
// ThreadBeginCritical/ThreadEndCritical selectors
// are directly observable and are modelled below
// with a real critical-section nesting counter
// instead of a blanket no-op.
//
// Per-trap Gestalt selectors (see arms below for cites):
// $A82A ComponentDispatch gestaltComponentMgr 'cpnt'
// $A9F8 MethodDispatch (OSL) (no documented selector)
// $AA53 DictionaryDispatch gestaltDictionaryMgrAttr 'dict'
// $AA54 TextServicesDispatch gestaltTSMgrVersion 'tsmv'
// $AA57 DockingDispatch (no documented selector)
// $AA59 MixedModeDispatch (no documented selector — PPC bridge)
// $AA5A CodeFragmentDispatch gestaltCFMAttr 'cfrg'
// $ABC9 IconDispatch gestaltIconUtilitiesAttr 'icon'
// $ABF2 ThreadDispatch gestaltThreadMgrAttr 'thds' (partial)
// $ABFC TranslationDispatch gestaltTranslationMgrExists (response bit 0)
//
// Future work: pick the highest-impact
// dispatcher (CodeFragmentDispatch $AA5A is hot for any
// PowerPC fat binary) and enumerate its selectors with
// proper pop discipline + per-selector defensive defaults
// (NIL handles, resNotFound on resource-by-name lookups,
// etc.) following the Pack14 pattern. Until then, the
// remaining stubbed dispatchers keep the register-
// preservation + stack-untouched contract.
// ComponentDispatch ($A82A) — Component Manager
// Inside Macintosh: More Macintosh Toolbox (1993),
// pp. 6-6, 6-29, and 6-98:
// `Gestalt('cpnt', ...)` gates availability; component
// call glue uses `INLINE $2F3C, paramSize, callNum,
// $7000, $A82A`, which pushes a 4-byte selector word
// [paramSize:callNum] then traps.
// Selector convention: stack-pushed 4-byte word at SP+0
// (high word=paramSize bytes, low word=callNum). D0=0
// means "call my component"; D0!=0 means "Component
// Manager internal request" with selector in D0.
// Gestalt: `gestaltComponentMgr = 'cpnt'` returns the CM
// version (>=3 supports automatic version control,
// unregister, icon families).
//
// HLE behaviour: D0=0 (noErr). For component calls
// (D0 == 0), read `param_size = read_word(sp + 0)` from
// the selector long and pop `4 + 4 + param_size` bytes
// (selector + ComponentInstance + routine args) so the
// inline glue stays balanced. Internal manager requests
// (D0 != 0) still return noErr and leave the caller stack
// untouched. All non-D0 registers are preserved.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::componentdispatch_*
// ComponentDispatch ($A82A): MMTB 1993 ch.6 17215. Stack-pushed [paramSize:callNum] selector at SP+0; D0=0 = call component, D0!=0 = CM internal. Gestalt 'cpnt'. HLE: D0=0, registers + stack preserved.
(true, 0x02A) => {
let d0 = cpu.read_reg(Register::D0);
if d0 == 0 {
let sp = cpu.read_reg(Register::A7);
let param_size = u32::from(bus.read_word(sp));
cpu.write_reg(Register::A7, sp + 8 + param_size);
}
cpu.write_reg(Register::D0, 0);
Ok(())
}
// MethodDispatch ($A9F8) — Object Support Library
// Public MPW declaration:
// `pascal OSErr MethodDispatch(short selector) = {0xA9F8};`
// Inside Macintosh Volume VI (1991), Object Support
// Library appendix. The selector arrives in D0 and the
// selector-0 path is the observed public contract: return
// noErr, preserve A7, and leave non-D0 registers alone.
//
// No documented Gestalt selector — apps that use OSL
// typically check via NGetTrapAddress($A9F8) returning
// _Unimplemented vs a real handler.
//
// HLE behaviour: D0=0 (noErr), all other registers
// preserved, stack untouched. OSL is essentially dead
// since no shipping app uses it directly (only via the
// System Object Model wrappers).
//
// Regression coverage:
// methoddispatch_*
// MethodDispatch (OSL) ($A9F8): IM:VI Object Support Library. D0 selector = method ID, args on stack. No Gestalt selector — apps probe via NGetTrapAddress. HLE: D0=0, registers + stack preserved.
(true, 0x1F8) => return_noerr(cpu),
// DictionaryDispatch ($AA53) — Dictionary Manager
// Inside Macintosh: Text 1993, ch. 8
// (Gestalt cite Text 1993 25656: "Use Gestalt with the
// gestaltDictionaryMgrAttr environment selector to obtain
// a result ... A result of gestaltDictionaryMgrPresent
// (= 0) means that the Dictionary Manager is present.")
// Selector convention: D0 = routine number per the
// Pack8/Pack14 `(arg_words << 8) | routine` encoding.
// Routines include InitializeDictionary, OpenDictionary,
// CloseDictionary, FindRecordInDictionary, etc.
// Gestalt: `gestaltDictionaryMgrAttr = 'dict'`,
// `gestaltDictionaryMgrPresent = 0`.
//
// HLE behaviour: selector 0x0500 (InitializeDictionary)
// writes noErr to the caller's function-result slot,
// returns noErr in D0, and pops the 10-byte public call
// frame (FSSpecPtr + maximumKeyLength + keyAttributes +
// script); all other selectors preserve registers and
// remain the no-op safety net. Apps that probe Gestalt
// first see "absent" and skip the dispatcher entirely.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::dictionarydispatch_*
// DictionaryDispatch ($AA53): Text 1993 ch. 8 25656, 26640-26648. D0 selector per (arg_words<<8)|routine. Gestalt 'dict' → gestaltDictionaryMgrPresent=0. HLE: InitializeDictionary writes noErr to the function-result slot, pops 10 bytes, and returns noErr; the remaining selectors stay stack-untouched.
(true, 0x253) => {
let selector = cpu.read_reg(Register::D0) & 0xFFFF;
let result = match selector {
0x0500 => {
let sp = cpu.read_reg(Register::A7);
bus.write_word(sp + 10, 0);
cpu.write_reg(Register::A7, sp + 10);
cpu.write_reg(Register::D0, 0);
Ok(())
}
_ => return_noerr(cpu),
};
result
}
// TextServicesDispatch ($AA54) — Text Services Manager
// Inside Macintosh: Text 1993, ch. 8
// (Gestalt cite Text 1993 22172: "Use the Gestalt
// environmental selector gestaltTSMgrVersion to determine
// whether the Text Services Manager is available.")
// The TSM routes input-method (e.g. CJKV IME, character
// palette) calls between an app and an installed text
// service component. Selector convention: D0 = routine
// number; selectors include InitTSMAwareApplication,
// CloseTSMAwareApplication, NewTSMDocument, etc.
// Gestalt: `gestaltTSMgrVersion = 'tsmv'` returns the
// TSM version as a 32-bit value.
//
// HLE behaviour: D0=0 (noErr), all other registers
// preserved, stack untouched. No current corpus title
// (English-only games) needs TSM.
//
// Regression coverage:
// textservicesdispatch_*
// TextServicesDispatch (TSM) ($AA54): Text 1993 ch.8 22172. D0 selector. Gestalt 'tsmv' → version word. HLE: D0=0, registers + stack preserved.
(true, 0x254) => return_noerr(cpu),
// DockingDispatch ($AA57) — Docking Manager (PowerBook)
// Inside Macintosh Volume VI (PowerBook docking station
// protocol). Specific to docking-station-aware PowerBooks
// (Duo 2x0 series); routes dock/undock notifications and
// power-state events. Selector convention: D0 = routine
// number per the standard System 7 dispatcher pattern.
// No documented Gestalt selector — apps probe via
// NGetTrapAddress($AA57) returning _Unimplemented when
// running on non-PowerBook hardware.
//
// HLE behaviour: D0=0 (noErr), all other registers
// preserved, stack untouched. No current corpus title
// is PowerBook-specific.
//
// Regression coverage:
// dockingdispatch_*
// DockingDispatch ($AA57): IM:VI PowerBook docking. D0 selector. No Gestalt selector — probe via NGetTrapAddress. HLE: D0=0, registers + stack preserved.
(true, 0x257) => {
cpu.write_reg(Register::D0, 0);
Ok(())
}
// MixedModeDispatch ($AA59) — Mixed Mode Manager
// Inside Macintosh: PowerPC System Software 1994
// (PPC SS 1994 line 2774: `_MixedModeMagic` is the
// mixed-mode A-trap that bridges 68K → PowerPC and
// PowerPC → 68K calls through Universal Procedure
// Pointers). Selector convention: A0 = UniversalProcPtr
// record; D0 = routine number for some MMM-internal
// services (CallUniversalProc / NewRoutineDescriptor /
// DisposeRoutineDescriptor).
// No documented Gestalt selector — MMM presence is
// implied by gestaltCFMAttr (CFM and MMM ship together).
//
// HLE behaviour: D0=0 (noErr), all other registers
// preserved, stack untouched. Systemless is a 68K-only HLE,
// so any MMM call (a PPC-side caller transitioning back
// to 68K) is unreachable in practice.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::mixedmodedispatch_returns_noerr_and_preserves_stack_pointer
// MixedModeDispatch ($AA59): PPC SS 1994 ch.7 2774 (`_MixedModeMagic`). A0=UPP record, D0 selector. No Gestalt — implied by 'cfrg'. HLE: D0=0, registers + stack preserved (68K-only HLE, MMM unreachable).
(true, 0x259) => return_noerr(cpu),
// CodeFragmentDispatch ($AA5A) — Code Fragment Manager
// Inside Macintosh: PowerPC System Software 1994
// (PPC SS 1994 ch.6, Gestalt cite line 1770: "if you
// need to know whether the Code Fragment Manager is
// available, you can call the Gestalt function with the
// selector gestaltCFMAttr"; constant cite line 4736:
// `#define gestaltCFMAttr 'cfrg'`).
// The CFM resolves and connects PowerPC code fragments
// ('cfrg' resources) — the loader for PowerPC native
// executables and shared libraries (PEF format).
// Selector convention: D0 = routine number; routines
// include GetSharedLibrary, GetDiskFragment, FindSymbol,
// CountSymbols, GetIndSymbol, CloseConnection, etc.
// Gestalt: `gestaltCFMAttr = 'cfrg'`,
// `gestaltCFMPresent = 0` (response bit 0).
//
// HLE behaviour: D0=0 (noErr), all other registers
// preserved, stack untouched. Systemless is a 68K-only HLE
// — apps that probe Gestalt see 'cfrg' undefined and
// either fall back to the 68K code path or refuse to
// launch. PPC fat binaries with 68K-fork still execute
// because the loader picks the 68K fork when CFM is
// absent.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::codefragmentdispatch_*
// CodeFragmentDispatch (CFM) ($AA5A): PPC SS 1994 ch.6 1770. D0 selector. Gestalt 'cfrg' → gestaltCFMPresent=0. HLE: D0=0, registers + stack preserved (68K-only — fat binaries fall back to 68K fork).
(true, 0x25A) => {
return_noerr(cpu)
}
// IconDispatch ($ABC9) — Icon Utilities
// Inside Macintosh: More Macintosh Toolbox (1993),
// pp. 5-18 and 5-71:
// Icon Utilities availability is gated by
// `gestaltIconUtilitiesAttr`, and the routines are invoked
// through `_IconDispatch` with routine selectors.
// Selector convention: D0 = routine number; routines
// include PlotIconID, NewIconSuite, AddIconToSuite,
// GetIconFromSuite, ForEachIconDo, GetIconCacheData,
// SetIconCacheData, IconIDToRgn, IconSuiteToRgn,
// IconMethodToRgn, etc. Note that GetIcon ($A9BB),
// PlotIcon ($A94B), GetCIcon ($AA1F), PlotCIcon ($AA1E),
// and DisposeCIcon ($AA25) are SEPARATE legacy traps
// available in System 6 and System 7 — those are NOT
// routed through IconDispatch.
// Gestalt: `gestaltIconUtilitiesAttr = 'icon'`.
//
// HLE behaviour: D0=0 (noErr), all other registers
// preserved, and the dispatcher advances A7 by 8 bytes
// on return. Apps that probe Gestalt see "absent" and
// fall back to the legacy trap surface for monochrome /
// color icons (which Systemless implements via $A9BB
// GetIcon / $AA1F GetCIcon / etc.).
//
// Regression coverage:
// src/trap/toolbox.rs::tests::icondispatch_*
// IconDispatch ($ABC9): MMTB 1993 ch.5 14879+15191. D0 selector. Gestalt 'icon' → gestaltIconUtilitiesPresent. HLE: selector $0000 returns noErr and pops the inline selector frame; unsupported selectors return paramErr (-50) and still pop the same frame. Apps fall back to legacy $A9BB/$AA1F monochrome/color icon traps.
(true, 0x3C9) => {
let selector = cpu.read_reg(Register::D0) & 0xFFFF;
match selector {
0 => return_noerr_and_pop(cpu, 8),
_ => return_error_and_pop(cpu, 8, -50),
}
}
// ThreadDispatch ($ABF2) — Thread Manager critical sections
// Inside Macintosh: Operating System Utilities 1994,
// Thread Manager chapter (pp. 69-70):
// pascal OSErr ThreadBeginCritical(void);
// pascal OSErr ThreadEndCritical(void);
// The public MPW glue dispatches those no-arg routines
// through `_ThreadDispatch` selectors $000B and $000C.
// MPW uses `#pragma parameter __D0` for the selector
// thunk, so the selector arrives in D0 and the stack is
// untouched.
//
// HLE behaviour: selector $000B increments the dispatcher-
// wide critical-section nesting counter and returns noErr.
// Selector $000C decrements the counter when nonzero and
// returns noErr; when the counter is already zero it
// returns threadProtocolErr (-619). Unsupported selectors
// return paramErr (-50). The public `Threads.h` wrappers
// read the 16-bit OSErr from the caller's zero-arg result
// slot at SP, so we mirror the low word there as well as
// in D0.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::threaddispatch_begin_and_end_critical_roundtrip
// src/trap/toolbox.rs::tests::threaddispatch_endcritical_underflow_returns_thread_protocol_err
// src/trap/toolbox.rs::tests::threaddispatch_unsupported_selector_returns_param_err
(true, 0x3F2) => {
let selector = cpu.read_reg(Register::D0) & 0xFFFF;
let sp = cpu.read_reg(Register::A7);
let result = match selector {
0x000B => {
self.thread_critical_nesting =
self.thread_critical_nesting.saturating_add(1);
0
}
0x000C if self.thread_critical_nesting == 0 => -619,
0x000C => {
self.thread_critical_nesting -= 1;
0
}
_ => -50,
};
bus.write_word(sp, result as u16);
if result == 0 {
return_noerr(cpu)
} else {
return_error_and_pop(cpu, 0, result)
}
}
// TranslationDispatch ($ABFC) — Translation Manager
// Inside Macintosh: More Macintosh Toolbox (1993),
// pp. 7-12 and 7-66:
// `Gestalt('xlat', ...)` with response bit
// `gestaltTranslationMgrExists` gates availability; the
// manager's routines are invoked through
// `_TranslationDispatch` routine selectors.
// The Translation Manager (Mac Easy Open / Macintosh
// Easy Open) routes file-format translation requests
// — e.g. MS Word .doc → Mac Word .mcw, JPEG → PICT.
// Selector convention: D0 = routine number per the
// selector summary table at MMTB 20828ff. Routines
// include GetTranslationExtensions, IdentifyFile,
// TranslateFile, TranslateContents, NewScriptingFile.
// Gestalt: `gestaltTranslationAttr` (response bit 0
// `gestaltTranslationMgrExists`).
//
// HLE behaviour: the dispatcher returns a non-zero error
// and advances A7 by 4 bytes. Apps that probe Gestalt
// see "absent" and either ask the user to convert the
// file manually or refuse to open foreign formats.
//
// Regression coverage:
// src/trap/toolbox.rs::tests::translationdispatch_*
// TranslationDispatch ($ABFC): MMTB 1993 ch.21 20111+20828. D0 selector per MMTB 20828 selector summary. Gestalt → gestaltTranslationMgrExists=0. HLE: D0=0, registers + stack preserved (apps refuse foreign-format opens).
(true, 0x3FC) => return_error_and_pop(cpu, 4, -50),
_ => return None,
})
}
/// Shared implementation of UniqueID ($A9C1) and Unique1ID ($A810).
///
/// Pascal signature is identical for both:
/// FUNCTION (theType: ResType): INTEGER;
/// Stack on entry:
/// SP theType (ResType, 4 bytes)
/// SP + 4 result (INTEGER, 2 bytes — caller-allocated)
/// Pops 4 bytes of args, leaves the 2-byte result slot.
///
/// `current_only = false` ($A9C1) scans every open resource file.
/// `current_only = true` ($A810) restricts the used-ID set to the
/// current resource file only, per IM:IV-16.
///
/// Always starts the candidate scan at 128 to sidestep the IM:I-121
/// caller-warning about IDs in the system-reserved range 0..127.
fn handle_unique_id<C: CpuOps>(
&mut self,
bus: &mut MacMemoryBus,
cpu: &mut C,
current_only: bool,
) -> Result<()> {
let sp = cpu.read_reg(Register::A7);
let raw_res_type = bus.read_long(sp).to_be_bytes();
let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
let mut used_ids = std::collections::HashSet::new();
if let Some(ref resources) = self.resources {
let mut chain: Vec<u16> = if current_only {
let refnum = self.current_resource_refnum();
if resources.files.contains_key(&refnum) {
vec![refnum]
} else {
Vec::new()
}
} else {
resources.files.keys().copied().collect()
};
chain.sort_unstable();
for refnum in chain {
if let Some(file) = resources.files.get(&refnum) {
for (t, id) in file.loaded.keys() {
if *t == res_type {
used_ids.insert(*id);
}
}
}
}
}
let mut candidate: i16 = 128;
while used_ids.contains(&candidate) {
candidate = candidate.wrapping_add(1);
if candidate <= 0 {
candidate = 128;
break;
}
}
bus.write_word(sp + 4, candidate as u16);
cpu.write_reg(Register::A7, sp + 4);
Ok(())
}
/// Shared implementation of GetIndType ($A99F) and Get1IndType ($A80F).
///
/// Pascal signature is identical for both:
/// PROCEDURE (VAR theType: ResType; index: INTEGER);
/// Pascal arg push order is left-to-right, so theType (the VAR ptr)
/// is pushed first (deeper) and index is pushed last (shallower).
/// Stack on entry:
/// SP index (INTEGER, 2 bytes)
/// SP + 2 typePtr (ResType*, 4 bytes)
/// Pops 6 bytes of args, no result slot (PROCEDURE).
///
/// `current_only = false` ($A99F) walks `resource_search_order()` —
/// the current resource file plus every file opened before it.
/// `current_only = true` ($A80F) restricts the walk to the current
/// resource file only, per IM:IV-15.
///
/// Uses BTreeSet so the index→type mapping is deterministic for a
/// given resource map; HashSet would be undefined-order and would
/// make tests flaky across rebuilds.
fn handle_get_ind_type<C: CpuOps>(
&mut self,
bus: &mut MacMemoryBus,
cpu: &mut C,
current_only: bool,
) -> Result<()> {
let sp = cpu.read_reg(Register::A7);
let index = bus.read_word(sp) as i16;
let type_ptr = bus.read_long(sp + 2);
let result_type = if let Some(ref resources) = self.resources {
let chain: Vec<u16> = if current_only {
let refnum = self.current_resource_refnum();
if resources.files.contains_key(&refnum) {
vec![refnum]
} else {
Vec::new()
}
} else {
self.resource_search_order()
};
let mut types = std::collections::BTreeSet::new();
for refnum in chain {
if let Some(file) = resources.files.get(&refnum) {
for (res_type, _) in file.loaded.keys() {
types.insert(*res_type);
}
}
}
if index >= 1 && (index as usize) <= types.len() {
types.into_iter().nth((index - 1) as usize)
} else {
None
}
} else {
None
};
if type_ptr != 0 {
match result_type {
Some(t) => bus.write_long(type_ptr, u32::from_be_bytes(t)),
None => bus.write_long(type_ptr, 0),
}
}
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
/// Shared implementation of GetIndResource ($A99D) and Get1IndResource ($A80E).
///
/// Pascal signature is identical for both:
/// FUNCTION (theType: ResType; index: INTEGER): Handle;
/// Stack on entry (16-bit aligned):
/// SP index (INTEGER, 2 bytes)
/// SP + 2 theType (ResType, 4 bytes)
/// SP + 6 result (Handle, 4 bytes — caller-allocated)
/// Pops 6 bytes of args, leaves the 4-byte result slot.
///
/// `current_only = false` ($A99D) walks `resource_search_order()` —
/// the current resource file plus every file opened before it.
/// `current_only = true` ($A80E) restricts the walk to the
/// current resource file only, per IM:IV-15.
fn handle_get_ind_resource<C: CpuOps>(
&mut self,
bus: &mut MacMemoryBus,
cpu: &mut C,
current_only: bool,
) -> Result<()> {
let trap_name = if current_only {
"Get1IndResource"
} else {
"GetIndResource"
};
let sp = cpu.read_reg(Register::A7);
let index = bus.read_word(sp) as i16;
let raw_res_type = bus.read_long(sp + 2).to_be_bytes();
let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
let type_str = String::from_utf8_lossy(&res_type);
if trace_sound_enabled()
&& self
.resources
.as_ref()
.is_some_and(|resources| resources.files.len() > 1)
{
eprintln!(
"[RSRC] {} raw='{}' norm='{}' index={} current={} current_only={}",
trap_name,
String::from_utf8_lossy(&raw_res_type),
type_str,
index,
self.current_resource_refnum(),
current_only,
);
}
// Build the candidate (id, refnum, ptr) list per IM:I I-116 / IV-15.
// For Get1IndResource ($A80E) the chain collapses to a single
// entry — the current resource file — so a multi-file scenario
// never leaks resources from inactive files into the index.
let candidates: Option<Vec<(i16, u16, u32)>> = self.resources.as_ref().map(|resources| {
let chain: Vec<u16> = if current_only {
let refnum = self.current_resource_refnum();
if resources.files.contains_key(&refnum) {
vec![refnum]
} else {
Vec::new()
}
} else {
self.resource_search_order()
};
let mut out: Vec<(i16, u16, u32)> = chain
.into_iter()
.filter_map(|refnum| resources.files.get(&refnum).map(|file| (refnum, file)))
.flat_map(|(refnum, file)| {
file.loaded
.iter()
.filter(|((t, _), _)| *t == res_type)
.map(move |((_, id), ptr)| (*id, refnum, *ptr))
.collect::<Vec<_>>()
})
.collect();
// Stable, deterministic order: by resource id ascending. Real
// ROM iterates the resource map in map order, but Systemless's
// HashMap-backed loaded table has no map order to speak of —
// sorting by id is the documented Get*IndResource contract
// ("returns handles to all resources of the given type", IM:IV-15)
// and matches the in-file behaviour of the existing
// GetIndResource arm prior to this split.
out.sort_by_key(|&(id, _, _)| id);
out
});
if let Some(candidates) = candidates {
if index >= 1 && (index as usize) <= candidates.len() {
let (res_id, _refnum, ptr) = candidates[(index - 1) as usize];
let handle = self.get_or_create_resource_handle(bus, res_type, res_id, ptr);
eprintln!(
"[TRAP] {}('{}', {}) -> id={} handle=${:08X}",
trap_name, type_str, index, res_id, handle
);
cpu.write_reg(Register::A0, handle);
cpu.write_reg(Register::D0, 0);
bus.write_word(0x0A60, 0); // ResErr = noErr per IM:I I-118
bus.write_long(sp + 6, handle);
cpu.write_reg(Register::A7, sp + 6);
self.maybe_inject_ajcp_decompress(bus, cpu, handle);
return Ok(());
}
}
eprintln!("[TRAP] {}('{}', {}) -> NULL", trap_name, type_str, index);
cpu.write_reg(Register::A0, 0);
cpu.write_reg(Register::D0, -192i32 as u32);
bus.write_word(0x0A60, (-192i16) as u16); // ResErr = resNotFound per IM:IV-15
Self::write_point_words(bus, sp + 6, Self::list_no_click_cell());
cpu.write_reg(Register::A7, sp + 6);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::dispatch::QueuedEvent;
use super::super::test_helpers::{setup, setup_with_port, TEST_SP};
use crate::cpu::{CpuOps, Register};
use crate::memory::MemoryBus;
use crate::trap::dispatch::{LoadedResources, ResourceFileMap};
use crate::trap::extended80::Extended80;
use std::collections::HashMap;
fn read_screen_pixel_1bpp(
bus: &crate::memory::MacMemoryBus,
screen_base: u32,
row_bytes: u32,
x: i16,
y: i16,
) -> bool {
let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
let bit = 7 - (x as u32 % 8);
let addr = screen_base + byte_offset;
(bus.read_byte(addr) & (1 << bit)) != 0
}
fn write_pascal_string(bus: &mut crate::memory::MacMemoryBus, addr: u32, text: &str) {
let bytes = text.as_bytes();
bus.write_byte(addr, bytes.len() as u8);
for (idx, byte) in bytes.iter().enumerate() {
bus.write_byte(addr + 1 + idx as u32, *byte);
}
}
#[test]
fn methoddispatch_returns_noerr_and_preserves_stack_pointer() {
// Inside Macintosh Volume VI (OSL appendix):
// MethodDispatch is the no-op dispatcher on selector 0.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0x1234_5678);
cpu.write_reg(Register::D1, 0x89AB_CDEF);
let result = disp.dispatch_toolbox(true, 0x1F8, &mut cpu, &mut bus);
assert!(result.is_some(), "MethodDispatch should be handled");
assert!(result.unwrap().is_ok(), "MethodDispatch should succeed");
assert_eq!(cpu.read_reg(Register::D0), 0, "MethodDispatch should return noErr");
assert_eq!(
cpu.read_reg(Register::D1),
0x89AB_CDEF,
"MethodDispatch should preserve non-D0 registers"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"MethodDispatch should preserve the caller stack pointer"
);
}
#[test]
fn codefragmentdispatch_returns_noerr_and_preserves_stack_pointer() {
// Inside Macintosh: PowerPC System Software 1994, Code Fragment
// Manager chapter. The selector-0 direct call is the safety-net path.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0x1234_5678);
let result = disp.dispatch_toolbox(true, 0x25A, &mut cpu, &mut bus);
assert!(result.is_some(), "CodeFragmentDispatch should be handled");
assert!(result.unwrap().is_ok(), "CodeFragmentDispatch should succeed");
assert_eq!(
cpu.read_reg(Register::D0),
0,
"CodeFragmentDispatch should return noErr"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"CodeFragmentDispatch should preserve the caller stack pointer"
);
}
#[test]
fn codefragmentdispatch_preserves_non_d0_registers_and_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D1, 0x2222_3333);
cpu.write_reg(Register::A0, 0x4444_5555);
cpu.write_reg(Register::A1, 0x6666_7777);
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0x0000_0000);
let result = disp.dispatch_toolbox(true, 0x25A, &mut cpu, &mut bus);
assert!(result.is_some(), "CodeFragmentDispatch should be handled");
assert!(result.unwrap().is_ok(), "CodeFragmentDispatch should return");
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::D1), 0x2222_3333);
assert_eq!(cpu.read_reg(Register::A0), 0x4444_5555);
assert_eq!(cpu.read_reg(Register::A1), 0x6666_7777);
assert_eq!(cpu.read_reg(Register::A7), sp_before);
}
#[test]
fn mixedmodedispatch_returns_noerr_and_preserves_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0x1234_5678);
cpu.write_reg(Register::D1, 0x2222_3333);
cpu.write_reg(Register::A0, 0x4444_5555);
cpu.write_reg(Register::A1, 0x6666_7777);
let result = disp.dispatch_toolbox(true, 0x259, &mut cpu, &mut bus);
assert!(result.is_some(), "MixedModeDispatch should be handled");
assert!(result.unwrap().is_ok(), "MixedModeDispatch should succeed");
assert_eq!(
cpu.read_reg(Register::D0),
0,
"MixedModeDispatch should return noErr"
);
assert_eq!(cpu.read_reg(Register::D1), 0x2222_3333);
assert_eq!(cpu.read_reg(Register::A0), 0x4444_5555);
assert_eq!(cpu.read_reg(Register::A1), 0x6666_7777);
assert_eq!(cpu.read_reg(Register::A7), sp_before);
}
// ========== QuickDraw Text Traps (Toolbox) ==========
#[test]
fn spaceextra_sets_current_port_spextra_field() {
// Inside Macintosh Volume I (1985), p. I-171:
// SpaceExtra sets the current GrafPort's spExtra field.
let (mut disp, mut cpu, mut bus) = setup();
let port = bus.alloc(128);
disp.current_port = port;
let extra = 0x0001_8000u32; // 1.5 Fixed
bus.write_long(port + 76, 0xDEAD_BEEF);
bus.write_long(TEST_SP, extra);
let result = disp.dispatch_toolbox(true, 0x08E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(port + 76), extra);
}
#[test]
fn spaceextra_consumes_fixed_argument_and_pops_four_bytes() {
// Inside Macintosh Volume I (1985), p. I-171:
// SpaceExtra(extra: Fixed) consumes one 4-byte Fixed argument.
let (mut disp, mut cpu, mut bus) = setup();
let port = bus.alloc(128);
disp.current_port = port;
let sp_before = cpu.read_reg(Register::A7);
bus.write_long(sp_before, 0xFFFF_8000); // -0.5 Fixed
let result = disp.dispatch_toolbox(true, 0x08E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
}
#[test]
fn piccomment_consumes_eight_byte_argument_frame() {
// Inside Macintosh Volume I (1985), p. I-190:
// PicComment(kind, dataSize, dataHandle) consumes an 8-byte frame.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
bus.write_word(sp_before, 0x1234);
bus.write_word(sp_before + 2, 0x0000);
bus.write_long(sp_before + 4, 0);
let result = disp.dispatch_toolbox(true, 0x0F2, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
}
#[test]
fn stuffhex_decodes_hex_pairs_into_destination_bytes() {
// Inside Macintosh Volume I (1985), p. I-195:
// StuffHex stores bits expressed as hexadecimal digits into a target structure.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src = bus.alloc(32);
let dst = bus.alloc(16);
let hex_bytes = *b"0102040810204080";
let expected = [0x01u8, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80];
bus.write_byte(src, hex_bytes.len() as u8);
for (i, byte) in hex_bytes.iter().enumerate() {
bus.write_byte(src + 1 + i as u32, *byte);
}
for i in 0..8u32 {
bus.write_byte(dst + i, 0xCC);
}
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, src);
bus.write_long(sp + 4, dst);
let result = disp.dispatch_toolbox(true, 0x066, &mut cpu, &mut bus);
assert!(result.is_some(), "StuffHex should be handled");
assert!(result.unwrap().is_ok(), "StuffHex should return");
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
for (i, byte) in expected.iter().enumerate() {
assert_eq!(bus.read_byte(dst + i as u32), *byte);
}
}
#[test]
fn stuffhex_consumes_thingptr_and_str255_arguments() {
// Inside Macintosh Volume I (1985), p. I-195:
// PROCEDURE StuffHex(thingPtr: Ptr; s: Str255) takes two pointer arguments.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_long(sp + 4, 0);
bus.write_word(sp + 8, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x066, &mut cpu, &mut bus);
assert!(result.is_some(), "StuffHex should be handled");
assert!(result.unwrap().is_ok(), "StuffHex should return");
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
assert_eq!(bus.read_word(sp + 8), 0xBEEF);
}
#[test]
fn ipclistports_zero_request_works_before_ppcinit() {
// Inside Macintosh: Interapplication Communication (1993),
// pp. 7-39, 7-41 to 7-42, 7-57.
let (mut disp, mut cpu, mut bus) = setup();
let pre_pb = bus.alloc(64);
let post_pb = bus.alloc(64);
let port_name = bus.alloc(64);
let location_name = bus.alloc(64);
let buffer = bus.alloc(64);
const IO_RESULT: u32 = 16;
const START_INDEX: u32 = 40;
const REQUEST_COUNT: u32 = 42;
const ACTUAL_COUNT: u32 = 44;
const PORT_NAME: u32 = 46;
const LOCATION_NAME: u32 = 50;
const BUFFER_PTR: u32 = 54;
bus.write_bytes(port_name, &[0; 64]);
bus.write_bytes(location_name, &[0; 64]);
bus.write_bytes(buffer, &[0; 64]);
bus.write_word(pre_pb + START_INDEX, 0);
bus.write_word(pre_pb + REQUEST_COUNT, 0);
bus.write_word(pre_pb + ACTUAL_COUNT, 0x1357);
bus.write_word(pre_pb + IO_RESULT, 0x2468);
bus.write_long(pre_pb + PORT_NAME, port_name);
bus.write_long(pre_pb + LOCATION_NAME, location_name);
bus.write_long(pre_pb + BUFFER_PTR, buffer);
cpu.write_reg(Register::A0, pre_pb);
cpu.write_reg(Register::D0, 0x000A);
let pre = disp.dispatch_toolbox(false, 0x0DD, &mut cpu, &mut bus);
assert!(pre.is_some(), "IPCListPorts should be handled");
assert!(pre.unwrap().is_ok(), "IPCListPorts should return");
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(pre_pb + IO_RESULT), 0);
assert_eq!(bus.read_word(pre_pb + ACTUAL_COUNT), 0);
assert!(!disp.ppc_initialized);
cpu.write_reg(Register::A0, 0);
cpu.write_reg(Register::D0, 0);
let init = disp.dispatch_toolbox(false, 0x0DD, &mut cpu, &mut bus);
assert!(init.is_some(), "PPCInit should be handled");
assert!(init.unwrap().is_ok(), "PPCInit should return");
assert!(disp.ppc_initialized);
assert_eq!(cpu.read_reg(Register::D0), 0);
bus.write_word(post_pb + START_INDEX, 0);
bus.write_word(post_pb + REQUEST_COUNT, 0);
bus.write_word(post_pb + ACTUAL_COUNT, 0x1357);
bus.write_word(post_pb + IO_RESULT, 0x2468);
bus.write_long(post_pb + PORT_NAME, port_name);
bus.write_long(post_pb + LOCATION_NAME, location_name);
bus.write_long(post_pb + BUFFER_PTR, buffer);
cpu.write_reg(Register::A0, post_pb);
cpu.write_reg(Register::D0, 0x000A);
let post = disp.dispatch_toolbox(false, 0x0DD, &mut cpu, &mut bus);
assert!(
post.is_some(),
"IPCListPorts should be handled after PPCInit"
);
assert!(
post.unwrap().is_ok(),
"IPCListPorts should return after PPCInit"
);
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(post_pb + IO_RESULT), 0);
assert_eq!(bus.read_word(post_pb + ACTUAL_COUNT), 0);
}
// Pack15 ($A831) — Picture Utilities
#[test]
fn pack15_newpictinfo_mints_distinct_nonzero_ids_and_dispospictinfo_returns_noerr() {
// Inside Macintosh Volume VI (1991), pp. 18-11 and 18-14:
// Pack15 uses a selector in D0; the stack carries only the
// Pascal arguments and the caller's function result slot.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let slot1 = bus.alloc(12);
let slot2 = bus.alloc(12);
let id1;
let id2;
let newpictinfo_first_ok;
let newpictinfo_second_ok;
let dispospictinfo_ok;
let dispospictinfo_double_noerr_ok;
bus.write_long(slot1, 0x1111_1111);
bus.write_long(slot1 + 4, 0xDEAD_BEEF);
bus.write_long(slot1 + 8, 0x2222_2222);
bus.write_long(slot2, 0x3333_3333);
bus.write_long(slot2 + 4, 0xFEED_FACE);
bus.write_long(slot2 + 8, 0x4444_4444);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x0602);
bus.write_word(sp, 0);
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_long(sp + 8, slot1 + 4);
bus.write_word(sp + 12, 0xBEEF);
let first = disp.dispatch_toolbox(true, 0x031, &mut cpu, &mut bus);
assert!(first.is_some(), "NewPictInfo should be handled");
assert!(first.unwrap().is_ok(), "NewPictInfo should return");
id1 = bus.read_long(slot1 + 4);
newpictinfo_first_ok = (id1 != 0)
&& (bus.read_long(slot1) == 0x1111_1111)
&& (bus.read_long(slot1 + 8) == 0x2222_2222)
&& (cpu.read_reg(Register::A7) == sp + 12)
&& (bus.read_word(sp + 12) == 0xBEEF);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x0602);
bus.write_word(sp, 0);
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_long(sp + 8, slot2 + 4);
bus.write_word(sp + 12, 0xCAFE);
let second = disp.dispatch_toolbox(true, 0x031, &mut cpu, &mut bus);
assert!(second.is_some(), "NewPictInfo should be handled");
assert!(second.unwrap().is_ok(), "NewPictInfo should return");
id2 = bus.read_long(slot2 + 4);
newpictinfo_second_ok = (id2 != 0)
&& (id2 != id1)
&& (bus.read_long(slot2) == 0x3333_3333)
&& (bus.read_long(slot2 + 8) == 0x4444_4444)
&& (cpu.read_reg(Register::A7) == sp + 12)
&& (bus.read_word(sp + 12) == 0xCAFE);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x0206);
bus.write_long(sp, id1);
bus.write_word(sp + 4, 0xFACE);
let third = disp.dispatch_toolbox(true, 0x031, &mut cpu, &mut bus);
assert!(third.is_some(), "DisposPictInfo should be handled");
assert!(third.unwrap().is_ok(), "DisposPictInfo should return");
dispospictinfo_ok = (cpu.read_reg(Register::A7) == sp + 4)
&& (cpu.read_reg(Register::D0) == 0)
&& (bus.read_word(sp + 4) == 0xFACE);
assert!(
newpictinfo_first_ok && newpictinfo_second_ok,
"A831:newpictinfo_mints_distinct_nonzero_ids_and_preserves_stack"
);
assert!(
dispospictinfo_ok,
"A831:dispospictinfo_returns_noerr_and_preserves_stack"
);
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x0206);
bus.write_long(sp, id1);
bus.write_word(sp + 4, 0xD00D);
let fourth = disp.dispatch_toolbox(true, 0x031, &mut cpu, &mut bus);
assert!(fourth.is_some(), "DisposPictInfo should be handled");
assert!(fourth.unwrap().is_ok(), "DisposPictInfo should return");
dispospictinfo_double_noerr_ok = (cpu.read_reg(Register::A7) == sp + 4)
&& (cpu.read_reg(Register::D0) == 0)
&& (bus.read_word(sp + 4) == 0xD00D);
assert!(
dispospictinfo_double_noerr_ok,
"A831:dispospictinfo_returns_noerr_on_double_dispose"
);
}
// ColorBit ($A864) — IM:I I-174 says the trap writes `whichBit` to the
// current grafPort's colrBit field. colrBit is a word-sized INTEGER at
// GrafPort offset +88 (Imaging With QuickDraw 1994, p. 4-39).
//
// Witnesses for the strict bake `a864_colorbit_strict`:
// tests::colorbit_writes_whichbit_value_to_current_port_colrbit_field_at_offset_88
// tests::colorbit_writes_max_31_value_to_current_port_colrbit_field
// tests::colorbit_zero_overwrites_previous_nonzero_colrbit_value
// tests::colorbit_consumes_two_byte_whichbit_argument_and_balances_stack
#[test]
fn colorbit_writes_whichbit_value_to_current_port_colrbit_field_at_offset_88() {
let (mut disp, mut cpu, mut bus) = setup_with_port();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
// Pre-poison adjacent fields to detect any over-write past the
// 2-byte colrBit word at port+88.
let port_ptr = 0x181000u32;
bus.write_long(port_ptr + 84, 0x0000001E); // bkColor (whiteColor=30)
bus.write_word(port_ptr + 88, 0); // colrBit initial value per IM:I I-174
bus.write_word(port_ptr + 90, 0xC3A5); // patStretch sentinel
bus.write_word(sp, 5);
let result = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
assert!(result.is_some(), "ColorBit must be a handled trap");
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_word(port_ptr + 88),
5,
"ColorBit(5) must write 5 to thePort^.colrBit"
);
assert_eq!(
bus.read_word(port_ptr + 90),
0xC3A5,
"ColorBit must not over-write past the 2-byte colrBit slot"
);
assert_eq!(
bus.read_long(port_ptr + 84),
0x0000001E,
"ColorBit must not corrupt the preceding bkColor field"
);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
}
#[test]
fn colorbit_writes_max_31_value_to_current_port_colrbit_field() {
// IM:I I-174: "the possible range of values for whichBit is 0 through 31"
let (mut disp, mut cpu, mut bus) = setup_with_port();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
let port_ptr = 0x181000u32;
bus.write_word(port_ptr + 88, 0);
bus.write_word(sp, 31);
let result = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(port_ptr + 88), 31);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
}
#[test]
fn colorbit_zero_overwrites_previous_nonzero_colrbit_value() {
// Defeats a stub that doesn't actually write the field: after a
// non-zero ColorBit, calling ColorBit(0) must clear it back to 0.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
let port_ptr = 0x181000u32;
// First ColorBit(7) — sets colrBit to 7.
bus.write_word(sp, 7);
let r1 = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
assert!(r1.unwrap().is_ok());
assert_eq!(bus.read_word(port_ptr + 88), 7);
// Reset SP for the second call.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0);
let r2 = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
assert!(r2.unwrap().is_ok());
assert_eq!(bus.read_word(port_ptr + 88), 0);
}
#[test]
fn colorbit_consumes_two_byte_whichbit_argument_and_balances_stack() {
// Pascal PROCEDURE protocol: caller pushes 2-byte INTEGER whichBit;
// trap pops 2 bytes; no function-result slot.
let (mut disp, mut cpu, mut bus) = setup_with_port();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 12);
bus.write_word(sp + 2, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
sp + 2,
"ColorBit must pop exactly the 2-byte INTEGER argument"
);
assert_eq!(
bus.read_word(sp + 2),
0xBEEF,
"ColorBit must not over-write past the 2-byte arg slot"
);
}
#[test]
fn longmul_writes_signed_64bit_product_to_dest_hilong_lolong() {
// Inside Macintosh Volume I (1985), p. I-472:
// LongMul writes signed Int64Bit { hiLong, loLong } product output.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let dest = bus.alloc(8);
let a: i32 = -123_456_789;
let b: i32 = 42_424;
let expected = (a as i64) * (b as i64);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, dest);
bus.write_long(sp + 4, b as u32);
bus.write_long(sp + 8, a as u32);
let result = disp.dispatch_toolbox(true, 0x067, &mut cpu, &mut bus);
assert!(result.is_some(), "LongMul should be handled");
assert!(result.unwrap().is_ok(), "LongMul should return");
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
assert_eq!(bus.read_long(dest), (expected >> 32) as u32);
assert_eq!(bus.read_long(dest + 4), expected as u32);
}
#[test]
fn longmul_consumes_a_b_and_dest_arguments() {
// Inside Macintosh Volume I (1985), p. I-472:
// PROCEDURE LongMul(a, b: LONGINT; VAR dest: Int64Bit) consumes 12 bytes.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_long(sp + 4, 2);
bus.write_long(sp + 8, 3);
bus.write_word(sp + 12, 0xCAFE);
let result = disp.dispatch_toolbox(true, 0x067, &mut cpu, &mut bus);
assert!(result.is_some(), "LongMul should be handled");
assert!(result.unwrap().is_ok(), "LongMul should return");
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
assert_eq!(bus.read_word(sp + 12), 0xCAFE);
}
// ========== Toolbox Event Traps ==========
// GetNextEvent ($A970) — empty queue
#[test]
fn test_get_next_event_empty() {
let (mut disp, mut cpu, mut bus) = setup();
disp.sent_open_app_event = true; // suppress synthetic oapp event
let sp = TEST_SP;
// SP+0: event_ptr(4), SP+4: eventMask(2), SP+6: result(2)
let event_ptr = 0x200000u32;
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0xFFFF); // eventMask: all
bus.write_word(sp + 6, 0xBEEF); // result placeholder
let result = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// No event: result = 0
assert_eq!(bus.read_word(sp + 6), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
// GetNextEvent ($A970) — with mouseDown event
#[test]
fn test_get_next_event_with_event() {
let (mut disp, mut cpu, mut bus) = setup();
disp.sent_open_app_event = true; // suppress synthetic oapp event
let sp = TEST_SP;
let event_ptr = 0x200000u32;
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0xFFFF); // eventMask: all
bus.write_word(sp + 6, 0x0000); // result placeholder
// Push a mouseDown event
disp.event_queue.push_back(QueuedEvent {
what: 1,
message: 0,
where_v: 10,
where_h: 20,
modifiers: 0,
});
let result = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// Event found: result = 0xFFFF
assert_eq!(bus.read_word(sp + 6), 0xFFFF);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
// Event record: what field at event_ptr+0 (word) = 1 (mouseDown)
assert_eq!(bus.read_word(event_ptr), 1);
}
#[test]
fn slotmanager_sreadinfo_selector_uses_a0_spblock_d0_selector_and_returns_oserr_in_d0() {
// Inside Macintosh: Devices (1994), pp. 2-61 to 2-62:
// _SlotManager selector $0010 (SReadInfo) uses A0=SpBlockPtr and
// D0=selector on entry, and returns OSErr in D0 on exit.
let (mut disp, mut cpu, mut bus) = setup();
let sp_block_ptr = 0x0031_0000u32;
let stack_ptr = 0x00F0_6100u32;
let selector = 0x0010u32; // SReadInfo
let sm_empty_slot = (-300i32) as u32;
bus.write_long(sp_block_ptr, 0xA5A5_A5A5);
bus.write_byte(sp_block_ptr + 49, 0x0A); // spSlot
cpu.write_reg(Register::A0, sp_block_ptr);
cpu.write_reg(Register::D0, selector);
cpu.write_reg(Register::A7, stack_ptr);
let result = disp.dispatch_toolbox(false, 0x06E, &mut cpu, &mut bus);
assert!(result.is_some(), "SlotManager should be handled");
assert!(
result.unwrap().is_ok(),
"SlotManager should return normally"
);
assert_eq!(
cpu.read_reg(Register::D0),
sm_empty_slot,
"SlotManager should return OSErr in D0"
);
assert_eq!(
cpu.read_reg(Register::A0),
sp_block_ptr,
"SlotManager should consume but not rewrite A0 SpBlock pointer"
);
assert_eq!(
cpu.read_reg(Register::A7),
stack_ptr,
"SlotManager register calling convention should preserve A7"
);
}
#[test]
fn slotmanager_sreadinfo_empty_slot_returns_smemptyslot() {
// Inside Macintosh: Devices (1994), pp. 2-61 to 2-62:
// SReadInfo result code smEmptySlot (-300) means "No card in this slot."
let (mut disp, mut cpu, mut bus) = setup();
let sp_block_ptr = 0x0031_0100u32;
let sm_empty_slot = (-300i32) as u32;
for slot in [0x09u8, 0x0Au8] {
bus.write_long(sp_block_ptr, 0x1122_3344);
bus.write_byte(sp_block_ptr + 49, slot); // spSlot
cpu.write_reg(Register::A0, sp_block_ptr);
cpu.write_reg(Register::D0, 0x0010); // SReadInfo selector
let result = disp.dispatch_toolbox(false, 0x06E, &mut cpu, &mut bus);
assert!(result.is_some(), "SlotManager should be handled");
assert!(
result.unwrap().is_ok(),
"SlotManager should return normally"
);
assert_eq!(
cpu.read_reg(Register::D0),
sm_empty_slot,
"SReadInfo selector should return smEmptySlot for empty slot {}",
slot
);
}
}
#[test]
fn slotmanager_writes_result_to_spblock_spresult_offset_zero() {
// Inside Macintosh: Devices (1994), pp. 2-23 to 2-24:
// SpBlock starts with spResult at offset 0.
let (mut disp, mut cpu, mut bus) = setup();
let sp_block_ptr = 0x0031_0200u32;
let sm_empty_slot = (-300i32) as u32;
bus.write_long(sp_block_ptr, 0xDEAD_BEEF); // spResult (offset 0)
bus.write_long(sp_block_ptr + 4, 0xBEEF_DEAD); // spsPointer (offset 4)
cpu.write_reg(Register::A0, sp_block_ptr);
cpu.write_reg(Register::D0, 0x0010); // SReadInfo selector
let result = disp.dispatch_toolbox(false, 0x06E, &mut cpu, &mut bus);
assert!(result.is_some(), "SlotManager should be handled");
assert!(
result.unwrap().is_ok(),
"SlotManager should return normally"
);
assert_eq!(
bus.read_long(sp_block_ptr),
sm_empty_slot,
"SlotManager should mirror the result into SpBlock.spResult"
);
assert_eq!(
bus.read_long(sp_block_ptr + 4),
0xBEEF_DEAD,
"SlotManager should not clobber adjacent SpBlock fields"
);
}
#[test]
fn slotmanager_other_selector_leaves_spblock_result_untouched() {
// Systemless's SlotManager HLE models the documented SReadInfo
// selector for the empty-slot result and leaves SpBlock state
// alone for other selectors that still collapse to smEmptySlot.
// This pins the selector-specific writeback rule in the HLE.
let (mut disp, mut cpu, mut bus) = setup();
let sp_block_ptr = 0x0031_0300u32;
let sm_empty_slot = (-300i32) as u32;
bus.write_long(sp_block_ptr, 0xDEAD_BEEF);
bus.write_long(sp_block_ptr + 4, 0xBEEF_DEAD);
cpu.write_reg(Register::A0, sp_block_ptr);
cpu.write_reg(Register::D0, 0x0000);
let result = disp.dispatch_toolbox(false, 0x06E, &mut cpu, &mut bus);
assert!(result.is_some(), "SlotManager should be handled");
assert!(
result.unwrap().is_ok(),
"SlotManager should return normally"
);
assert_eq!(
cpu.read_reg(Register::D0),
sm_empty_slot,
"SlotManager should still return smEmptySlot"
);
assert_eq!(
bus.read_long(sp_block_ptr),
0xDEAD_BEEF,
"non-SReadInfo selectors should not rewrite SpBlock.spResult"
);
assert_eq!(
bus.read_long(sp_block_ptr + 4),
0xBEEF_DEAD,
"non-SReadInfo selectors should not clobber adjacent SpBlock fields"
);
}
// Inside Macintosh Volume I, I-257: events not designated by eventMask are
// kept in the queue and a null event is returned when no designated event
// is available.
#[test]
fn test_get_next_event_mask_miss_returns_null_and_preserves_queue() {
let (mut disp, mut cpu, mut bus) = setup();
disp.sent_open_app_event = true; // suppress synthetic oapp event
let sp = TEST_SP;
let event_ptr = 0x200000u32;
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0x0008); // keyDownMask (what=3)
bus.write_word(sp + 6, 0xBEEF);
disp.event_queue.push_back(QueuedEvent {
what: 1, // mouseDown (not in keyDownMask)
message: 0,
where_v: 44,
where_h: 88,
modifiers: 0,
});
let first = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
assert!(first.is_some());
assert!(first.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0);
assert_eq!(bus.read_word(event_ptr), 0); // nullEvt
assert_eq!(disp.event_queue.len(), 1);
assert_eq!(disp.event_queue.front().map(|event| event.what), Some(1));
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0x0002); // mouseDownMask (what=1)
bus.write_word(sp + 6, 0x0000);
let second = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
assert!(second.is_some());
assert!(second.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0xFFFF);
assert_eq!(bus.read_word(event_ptr), 1);
assert!(disp.event_queue.is_empty());
}
// WaitNextEvent ($A860) — empty queue
#[test]
fn test_wait_next_event_empty() {
let (mut disp, mut cpu, mut bus) = setup();
// Mark the synthetic kAEOpenApplication as already sent so this tests
// the normal empty-queue path.
disp.sent_open_app_event = true;
let sp = TEST_SP;
// SP+0: mouseRgn(4), SP+4: sleep(4), SP+8: event_ptr(4), SP+12: eventMask(2), SP+14: result(2)
bus.write_long(sp, 0); // mouseRgn
bus.write_long(sp + 4, 60); // sleep
bus.write_long(sp + 8, 0x200000); // event_ptr
bus.write_word(sp + 12, 0xFFFF); // eventMask
bus.write_word(sp + 14, 0xBEEF); // result placeholder
let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 14), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 14);
assert_eq!(disp.pending_wait_sleep_ticks, 60);
assert_eq!(disp.tick_count, 100);
assert_eq!(bus.read_long(0x200000 + 6), 100);
}
#[test]
fn test_wait_next_event_zero_mask_takes_null_event_path() {
let (mut disp, mut cpu, mut bus) = setup();
assert!(!disp.sent_open_app_event);
let sp = TEST_SP;
let event_ptr = 0x200000u32;
bus.write_word(event_ptr, 0x5555);
bus.write_long(sp, 0); // mouseRgn
bus.write_long(sp + 4, 1); // sleep
bus.write_long(sp + 8, event_ptr);
bus.write_word(sp + 12, 0); // eventMask: no event types selected
bus.write_word(sp + 14, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 14), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 14);
assert_eq!(bus.read_word(event_ptr), 0);
assert!(!disp.sent_open_app_event);
assert_eq!(disp.pending_wait_sleep_ticks, 1);
}
// WaitNextEvent ($A860) — synthesizes kAEOpenApplication on first call
#[test]
fn test_wait_next_event_synthesizes_open_app() {
let (mut disp, mut cpu, mut bus) = setup();
assert!(!disp.sent_open_app_event);
let sp = TEST_SP;
let event_ptr = 0x200000u32;
bus.write_long(sp, 0); // mouseRgn
bus.write_long(sp + 4, 0); // sleep
bus.write_long(sp + 8, event_ptr); // event_ptr
bus.write_word(sp + 12, 0x0400); // highLevelEventMask only (bit 10)
bus.write_word(sp + 14, 0x0000); // result placeholder
let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// Should return TRUE (event found)
assert_eq!(bus.read_word(sp + 14), 0xFFFF);
// what = kHighLevelEvent (23)
assert_eq!(bus.read_word(event_ptr), 23);
// message = kCoreEventClass ('aevt' = 0x61657674)
assert_eq!(bus.read_long(event_ptr + 2), 0x61657674);
// where = kAEOpenApplication ('oapp' = 0x6F617070), packed as Point (v, h)
assert_eq!(bus.read_word(event_ptr + 10), 0x6F61); // where.v
assert_eq!(bus.read_word(event_ptr + 12), 0x7070); // where.h
// Flag should be set
assert!(disp.sent_open_app_event);
// Second call should NOT return synthetic event
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
bus.write_long(sp + 4, 0);
bus.write_long(sp + 8, event_ptr);
bus.write_word(sp + 12, 0x0400);
bus.write_word(sp + 14, 0x0000);
let result2 = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
assert!(result2.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 14), 0); // no event
}
// EventAvail ($A971) — with event (peeks, does not remove)
#[test]
fn test_event_avail_with_event() {
let (mut disp, mut cpu, mut bus) = setup();
disp.sent_open_app_event = true; // suppress synthetic oapp event
let sp = TEST_SP;
let event_ptr = 0x200000u32;
// SP+0: event_ptr(4), SP+4: eventMask(2), SP+6: result(2)
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0xFFFF); // all events
bus.write_word(sp + 6, 0x0000);
disp.event_queue.push_back(QueuedEvent {
what: 1, // mouseDown
message: 0,
where_v: 5,
where_h: 15,
modifiers: 0,
});
let result = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0xFFFF);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
// Event record should have what=1
assert_eq!(bus.read_word(event_ptr), 1);
// Event should still be in the queue (peek, not dequeue)
assert_eq!(disp.event_queue.len(), 1);
}
// EventAvail ($A971) — empty queue
#[test]
fn test_event_avail_empty() {
let (mut disp, mut cpu, mut bus) = setup();
disp.sent_open_app_event = true; // suppress synthetic oapp event
let sp = TEST_SP;
let event_ptr = 0x200000u32;
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0xFFFF);
bus.write_word(sp + 6, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
assert_eq!(bus.read_word(event_ptr), 0);
assert_ne!(bus.read_word(event_ptr + 14) & 0x0080, 0);
}
// Inside Macintosh Volume I, I-257..I-259: EventAvail follows the same
// eventMask filtering as GetNextEvent and does not dequeue non-designated
// events.
#[test]
fn test_event_avail_mask_miss_returns_null_and_preserves_queue() {
let (mut disp, mut cpu, mut bus) = setup();
disp.sent_open_app_event = true; // suppress synthetic oapp event
let sp = TEST_SP;
let event_ptr = 0x200000u32;
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0x0008); // keyDownMask (what=3)
bus.write_word(sp + 6, 0xBEEF);
disp.event_queue.push_back(QueuedEvent {
what: 1, // mouseDown
message: 0x1234_5678,
where_v: 11,
where_h: 22,
modifiers: 0,
});
let result = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0);
assert_eq!(bus.read_word(event_ptr), 0); // nullEvt
assert_eq!(disp.event_queue.len(), 1);
assert_eq!(disp.event_queue.front().map(|event| event.what), Some(1));
}
// Inside Macintosh Volume I, I-259: EventAvail reports an event without
// removing it, so a following GetNextEvent can return that same event.
#[test]
fn test_event_avail_then_get_next_event_returns_same_event() {
let (mut disp, mut cpu, mut bus) = setup();
disp.sent_open_app_event = true; // suppress synthetic oapp event
let sp = TEST_SP;
let event_ptr = 0x200000u32;
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0x0002); // mouseDownMask
bus.write_word(sp + 6, 0x0000);
disp.event_queue.push_back(QueuedEvent {
what: 1,
message: 0xCAFEBABE,
where_v: 9,
where_h: 19,
modifiers: 0,
});
let peek = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
assert!(peek.is_some());
assert!(peek.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0xFFFF);
assert_eq!(bus.read_word(event_ptr), 1);
assert_eq!(bus.read_long(event_ptr + 2), 0xCAFEBABE);
assert_eq!(disp.event_queue.len(), 1);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0x0002); // mouseDownMask
bus.write_word(sp + 6, 0x0000);
let dequeue = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
assert!(dequeue.is_some());
assert!(dequeue.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0xFFFF);
assert_eq!(bus.read_word(event_ptr), 1);
assert_eq!(bus.read_long(event_ptr + 2), 0xCAFEBABE);
assert!(disp.event_queue.is_empty());
}
#[test]
fn test_event_avail_synthesizes_open_app_without_consuming() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let event_ptr = 0x200000u32;
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0x0400); // highLevelEventMask
bus.write_word(sp + 6, 0x0000);
let result = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0xFFFF);
assert_eq!(bus.read_word(event_ptr), 23);
assert_eq!(bus.read_long(event_ptr + 2), 0x61657674);
assert_eq!(disp.event_queue.len(), 1);
assert_eq!(disp.event_queue.front().map(|event| event.what), Some(23));
}
#[test]
fn test_get_next_event_synthesizes_open_app() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let event_ptr = 0x200000u32;
bus.write_long(sp, event_ptr);
bus.write_word(sp + 4, 0x0400); // highLevelEventMask
bus.write_word(sp + 6, 0x0000);
let result = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0xFFFF);
assert_eq!(bus.read_word(event_ptr), 23);
assert_eq!(bus.read_long(event_ptr + 2), 0x61657674);
assert!(disp.event_queue.is_empty());
}
// GetKeys ($A976)
#[test]
fn test_get_keys_reflects_pressed_keys() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let keys_ptr = 0x200100u32;
// Press left arrow (0x7B) and space (0x31)
disp.push_key_down(0x7B, 28);
disp.push_key_down(0x31, 32);
bus.write_long(sp, keys_ptr);
let result = disp.dispatch_toolbox(true, 0x176, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
let left_byte = (0x7B / 8) as u32;
let left_bit = 1u8 << (0x7B % 8);
assert_ne!(bus.read_byte(keys_ptr + left_byte) & left_bit, 0);
let space_byte = (0x31 / 8) as u32;
let space_bit = 1u8 << (0x31 % 8);
assert_ne!(bus.read_byte(keys_ptr + space_byte) & space_bit, 0);
// Release left arrow and verify it clears.
disp.push_key_up(0x7B, 28);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, keys_ptr);
let result = disp.dispatch_toolbox(true, 0x176, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_byte(keys_ptr + left_byte) & left_bit, 0);
assert_ne!(bus.read_byte(keys_ptr + space_byte) & space_bit, 0);
}
// GetMouse ($A972)
#[test]
fn test_get_mouse() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let pt_ptr = 0x200000u32;
bus.write_long(sp, pt_ptr);
disp.mouse_pos = (50, 100);
let result = disp.dispatch_toolbox(true, 0x172, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// Point: v at pt_ptr, h at pt_ptr+2
assert_eq!(bus.read_word(pt_ptr), 50);
assert_eq!(bus.read_word(pt_ptr + 2), 100);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// StillDown ($A973) — button pressed
#[test]
fn test_still_down_pressed() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0000);
disp.mouse_button = true;
let result = disp.dispatch_toolbox(true, 0x173, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp), 0xFFFF);
// SP unchanged for StillDown
assert_eq!(cpu.read_reg(Register::A7), sp);
}
// StillDown ($A973) — button not pressed
#[test]
fn test_still_down_not_pressed() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0xFFFF);
disp.mouse_button = false;
let result = disp.dispatch_toolbox(true, 0x173, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
}
// Button ($A974) — pressed (reads MBState $0172)
#[test]
fn test_button_pressed() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0000);
bus.write_byte(0x0172, 0x00); // MBState: button down
let result = disp.dispatch_toolbox(true, 0x174, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp), 0xFFFF);
assert_eq!(cpu.read_reg(Register::A7), sp);
}
// Button ($A974) — not pressed (reads MBState $0172)
#[test]
fn test_button_not_pressed() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0xFFFF);
bus.write_byte(0x0172, 0x80); // MBState: button up
let result = disp.dispatch_toolbox(true, 0x174, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
}
// Button reads MBState ($0172), which is updated by the runner at
// each tick advance (VBL analog). After push_mouse_up the internal
// mouse_button is false, but $0172 retains the pressed state until
// the next tick — matching real hardware VBL latency.
#[test]
fn test_button_reads_mbstate_not_internal_flag() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// Simulate: runner wrote $0172=0x00 on mouse-down
bus.write_byte(0x0172, 0x00);
disp.push_mouse_down(50, 100);
let _ = disp.dequeue_event(0xFFFF);
disp.push_mouse_up(50, 100);
// Internal state is released, but $0172 is still "pressed"
// (runner hasn't advanced a tick yet).
assert!(!disp.mouse_button);
bus.write_word(sp, 0);
let result = disp.dispatch_toolbox(true, 0x174, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp), 0xFFFF); // Button sees $0172 = pressed
// After the runner advances a tick, it would write $0172 = 0x80.
// Simulate that:
bus.write_byte(0x0172, 0x80);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0xFFFF);
let result = disp.dispatch_toolbox(true, 0x174, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp), 0); // Now Button sees released
}
// TickCount ($A975)
#[test]
fn test_tick_count() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// TickCount reads self.tick_count directly (kept in sync with
// $016A by advance_guest_tick). In production advance_guest_tick
// updates both; the test sets both here to match.
bus.write_long(0x016A, 12345);
disp.tick_count = 12345;
let result = disp.dispatch_toolbox(true, 0x175, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp), 12345);
// SP unchanged
assert_eq!(cpu.read_reg(Register::A7), sp);
}
#[test]
fn tickcount_returns_self_tick_count_and_does_not_advance_a7() {
// Per Macintosh Toolbox Essentials 1992 p. 2-112 the Pascal
// FUNCTION protocol pre-allocates a 4-byte LongInt result
// slot at SP+0; the trap writes there and the caller pops
// the result. Specifically the trap must NOT advance A7 —
// doing so would corrupt the C compiler's expected stack
// frame on return. This contract test pre-poisons SP+4 with
// a sentinel and verifies the trap leaves it untouched while
// writing the result to SP+0.
//
// Distinct from `test_tick_count` because that test confirms
// the value and the SP-non-advance, while this one also
// proves the trap does not write past the result slot — a
// regression guard against any future "fix" that uses
// bus.write_long(sp - 4, ...) or bus.write_long(sp + 4, ...).
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0xDEAD_BEEF); // pre-poison result slot
bus.write_long(sp + 4, 0xCAFE_BABE); // pre-poison past-result slot
bus.write_long(sp.wrapping_sub(4), 0xFEED_FACE); // pre-poison pre-result slot
disp.tick_count = 0x0000_4321;
bus.write_long(0x016A, 0x0000_4321);
let result = disp.dispatch_toolbox(true, 0x175, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
// The trap wrote the LongInt result to SP+0.
assert_eq!(bus.read_long(sp), 0x0000_4321);
// SP+4 sentinel preserved — no over-write past the result slot.
assert_eq!(bus.read_long(sp + 4), 0xCAFE_BABE);
// SP-4 sentinel preserved — no under-write before the result slot.
assert_eq!(bus.read_long(sp.wrapping_sub(4)), 0xFEED_FACE);
// A7 unchanged: Pascal FUNCTION result slot is consumed by caller.
assert_eq!(cpu.read_reg(Register::A7), sp);
}
#[test]
fn tickcount_returns_monotonically_nondecreasing_across_two_calls() {
// Per MTE 1992 p. 2-112: "The tick count is incremented during
// the vertical retrace interrupt"; IM:I I-260 warns to use
// ">= previous" comparisons. Systemless's HLE reads self.tick_count
// which is monotonic by construction (advance_guest_tick only
// increments). This contract test asserts that two consecutive
// dispatches with self.tick_count incremented between them
// return monotonic results.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
disp.tick_count = 1_000;
bus.write_long(0x016A, 1_000);
let r1 = disp.dispatch_toolbox(true, 0x175, &mut cpu, &mut bus);
assert!(r1.unwrap().is_ok());
let t1 = bus.read_long(sp);
// Advance tick globally (mirroring what advance_guest_tick does
// between trap dispatches in the runner).
disp.tick_count += 1;
bus.write_long(0x016A, disp.tick_count);
let r2 = disp.dispatch_toolbox(true, 0x175, &mut cpu, &mut bus);
assert!(r2.unwrap().is_ok());
let t2 = bus.read_long(sp);
assert!(t2 >= t1, "TickCount must be monotonic: t1={t1} t2={t2}");
assert_eq!(t1, 1_000);
assert_eq!(t2, 1_001);
}
// ========== Scrap Manager Traps ==========
// Inside Macintosh Volume I, I-457..I-459: InfoScrap returns ScrapStuff
// fields, ZeroScrap increments scrapCount, and PutScrap contributes bytes
// to scrapSize.
#[test]
fn infoscrap_reports_in_memory_scrapstate_and_entry_size() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0xDEAD_BEEF);
let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
assert!(zero.is_some());
assert!(zero.unwrap().is_ok());
assert_eq!(bus.read_long(sp), 0); // noErr
assert_eq!(cpu.read_reg(Register::A7), sp);
let source = bus.alloc(3);
bus.write_bytes(source, b"ABC");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, source);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 3);
bus.write_long(sp + 12, 0xDEAD_BEEF);
let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put.is_some());
assert!(put.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 12), 0); // noErr
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
let info = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
assert!(info.is_some());
assert!(info.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp);
let scrap_info = bus.read_long(sp);
assert_ne!(scrap_info, 0);
assert_eq!(bus.read_long(scrap_info), 12); // type(4)+len(4)+padded data(4)
let scrap_handle = bus.read_long(scrap_info + 4);
assert_ne!(
scrap_handle, 0,
"InfoScrap should expose a live Handle when the scrap is in memory"
);
let scrap_ptr = bus.read_long(scrap_handle);
assert_ne!(
scrap_ptr, 0,
"non-empty in-memory scrap should have a non-NIL master-pointer target"
);
assert_eq!(bus.get_alloc_size(scrap_ptr), Some(12));
assert_eq!(bus.read_word(scrap_info + 8), 1); // scrapCount after first ZeroScrap
assert_eq!(bus.read_word(scrap_info + 10), 1); // positive = in memory (IM:I I-457)
assert_eq!(bus.read_long(scrap_info + 12), 0); // scrapName = NIL in HLE
}
// Inside Macintosh Volume I, I-457: ScrapHandle is a handle to the desk
// scrap when the scrap is in memory. The serialized bytes are laid out as
// type(4) + length(4) + data + even-byte padding.
#[test]
fn infoscrap_scraphandle_serializes_current_entries() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0);
let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
assert!(zero.unwrap().is_ok());
let source = bus.alloc(3);
bus.write_bytes(source, b"ABC");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, source);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 3);
bus.write_long(sp + 12, 0);
let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
let info = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
assert!(info.unwrap().is_ok());
let scrap_info = bus.read_long(sp);
let scrap_handle = bus.read_long(scrap_info + 4);
assert_ne!(scrap_handle, 0);
let scrap_ptr = bus.read_long(scrap_handle);
assert_ne!(scrap_ptr, 0);
let expected = [b'T', b'E', b'X', b'T', 0, 0, 0, 3, b'A', b'B', b'C', 0];
assert_eq!(
bus.read_bytes(scrap_ptr, expected.len()),
expected.as_slice()
);
assert_eq!(bus.get_alloc_size(scrap_ptr), Some(expected.len() as u32));
}
// Inside Macintosh Volume I, I-458: ZeroScrap clears prior data and changes
// scrapCount in the InfoScrap record.
#[test]
fn zeroscrap_clears_contents_and_changes_scrapcount() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0);
let zero1 = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
assert!(zero1.unwrap().is_ok());
let source = bus.alloc(1);
bus.write_byte(source, b'X');
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, source);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 1);
bus.write_long(sp + 12, 0);
let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
let info_before = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
assert!(info_before.unwrap().is_ok());
let scrap_info = bus.read_long(sp);
assert_eq!(bus.read_long(scrap_info), 10); // 8 + padded(1 -> 2)
assert_eq!(bus.read_word(scrap_info + 8), 1);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xDEAD_BEEF);
let zero2 = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
assert!(zero2.unwrap().is_ok());
assert_eq!(bus.read_long(sp), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
let info_after = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
assert!(info_after.unwrap().is_ok());
let scrap_info_after = bus.read_long(sp);
assert_eq!(bus.read_long(scrap_info_after), 0);
assert_eq!(bus.read_word(scrap_info_after + 8), 2);
}
// Inside Macintosh Volume I, I-459: GetScrap returns byte length on
// success and reports data offset from start-of-scrap; NIL hDest queries
// length/offset without copying.
#[test]
fn getscrap_with_nil_handle_returns_length_and_data_offset() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0);
let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
assert!(zero.unwrap().is_ok());
let text_src = bus.alloc(1);
bus.write_byte(text_src, b'A');
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, text_src);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 1);
bus.write_long(sp + 12, 0);
let put_text = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put_text.unwrap().is_ok());
let pict_src = bus.alloc(3);
bus.write_bytes(pict_src, b"XYZ");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, pict_src);
bus.write_long(sp + 4, u32::from_be_bytes(*b"PICT"));
bus.write_long(sp + 8, 3);
bus.write_long(sp + 12, 0);
let put_pict = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put_pict.unwrap().is_ok());
let offset_ptr = bus.alloc(4);
bus.write_long(offset_ptr, 0xFFFF_FFFF);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, offset_ptr);
bus.write_long(sp + 4, u32::from_be_bytes(*b"PICT"));
bus.write_long(sp + 8, 0); // NIL handle query path
bus.write_long(sp + 12, 0xDEAD_BEEF);
let get = disp.dispatch_toolbox(true, 0x1FD, &mut cpu, &mut bus);
assert!(get.is_some());
assert!(get.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 12), 3);
assert_eq!(bus.read_long(offset_ptr), 18); // TEXT entry(10) + PICT header(8)
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
// Inside Macintosh Volume I, I-459: GetScrap returns noTypeErr (-102)
// when no data of the requested type exists.
#[test]
fn getscrap_missing_type_returns_notypeerr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let offset_ptr = bus.alloc(4);
bus.write_long(offset_ptr, 0x1234_5678);
bus.write_long(sp, offset_ptr);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 0);
bus.write_long(sp + 12, 0);
let get = disp.dispatch_toolbox(true, 0x1FD, &mut cpu, &mut bus);
assert!(get.is_some());
assert!(get.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 12) as i32, -102);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
// Inside Macintosh Volume I, I-459 warning: duplicate PutScrap type entries
// append, and GetScrap returns the first matching type.
#[test]
fn getscrap_duplicate_type_returns_first_occurrence() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0);
let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
assert!(zero.unwrap().is_ok());
let old_src = bus.alloc(3);
bus.write_bytes(old_src, b"OLD");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, old_src);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 3);
bus.write_long(sp + 12, 0);
let put_old = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put_old.unwrap().is_ok());
let new_src = bus.alloc(3);
bus.write_bytes(new_src, b"NEW");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, new_src);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 3);
bus.write_long(sp + 12, 0);
let put_new = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put_new.unwrap().is_ok());
let offset_ptr = bus.alloc(4);
let h_dest = bus.alloc(4);
bus.write_long(h_dest, 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, offset_ptr);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, h_dest);
bus.write_long(sp + 12, 0xDEAD_BEEF);
let get = disp.dispatch_toolbox(true, 0x1FD, &mut cpu, &mut bus);
assert!(get.is_some());
assert!(get.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 12), 3);
assert_eq!(bus.read_long(offset_ptr), 8);
let data_ptr = bus.read_long(h_dest);
assert_ne!(data_ptr, 0);
let bytes = [
bus.read_byte(data_ptr),
bus.read_byte(data_ptr + 1),
bus.read_byte(data_ptr + 2),
];
assert_eq!(&bytes, b"OLD");
}
// Inside Macintosh Volume I, I-459: given an existing minimum-size
// handle, GetScrap resizes it to hold the copied bytes and leaves the
// copied block owned by that same handle.
#[test]
fn getscrap_existing_handle_resizes_copy_and_preserves_ownership() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0);
let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
assert!(zero.unwrap().is_ok());
let text_src = bus.alloc(3);
bus.write_bytes(text_src, b"OLD");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, text_src);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 3);
bus.write_long(sp + 12, 0);
let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put.unwrap().is_ok());
cpu.write_reg(Register::D0, 1);
let new_handle = disp.dispatch_memory(false, 0x22, &mut cpu, &mut bus);
assert!(new_handle.is_some());
assert!(new_handle.unwrap().is_ok());
let h_dest = cpu.read_reg(Register::A0);
let old_ptr = bus.read_long(h_dest);
assert_ne!(old_ptr, 0);
bus.write_byte(old_ptr, b'Z');
let offset_ptr = bus.alloc(4);
bus.write_long(offset_ptr, 0xFFFF_FFFF);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, offset_ptr);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, h_dest);
bus.write_long(sp + 12, 0xDEAD_BEEF);
let get = disp.dispatch_toolbox(true, 0x1FD, &mut cpu, &mut bus);
assert!(get.is_some());
assert!(get.unwrap().is_ok());
let data_ptr = bus.read_long(h_dest);
assert_ne!(data_ptr, 0);
assert_eq!(bus.read_long(sp + 12), 3);
assert_eq!(bus.read_long(offset_ptr), 8);
assert_eq!(bus.get_alloc_size(data_ptr), Some(3));
assert_eq!(bus.read_bytes(data_ptr, 3), b"OLD");
cpu.write_reg(Register::A0, data_ptr);
let recover = disp.dispatch_memory(false, 0x28, &mut cpu, &mut bus);
assert!(recover.is_some());
assert!(recover.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A0), h_dest);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// Inside Macintosh Volume I, I-458: UnloadScrap/LoadScrap return noErr on
// success and the ScrapStuff record reflects the resident/on-disk state.
#[test]
fn unloadscrap_and_loadscrap_return_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0);
let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
assert!(zero.is_some());
assert!(zero.unwrap().is_ok());
let source = bus.alloc(3);
bus.write_bytes(source, b"ABC");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, source);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 3);
bus.write_long(sp + 12, 0);
let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put.is_some());
assert!(put.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
let info_before = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
assert!(info_before.is_some());
assert!(info_before.unwrap().is_ok());
let scrap_info_before = bus.read_long(sp);
let scrap_handle_before = bus.read_long(scrap_info_before + 4);
assert_ne!(scrap_handle_before, 0);
assert_eq!(bus.read_word(scrap_info_before + 10), 1);
assert_eq!(bus.read_long(scrap_info_before), 12);
assert_eq!(
bus.get_alloc_size(bus.read_long(scrap_handle_before)),
Some(12)
);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xDEAD_BEEF);
let unload = disp.dispatch_toolbox(true, 0x1FA, &mut cpu, &mut bus);
assert!(unload.is_some());
assert!(unload.unwrap().is_ok());
assert_eq!(bus.read_long(sp), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
let info_after_unload = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
assert!(info_after_unload.is_some());
assert!(info_after_unload.unwrap().is_ok());
let scrap_info_after_unload = bus.read_long(sp);
assert_eq!(bus.read_long(scrap_info_after_unload), 12);
assert_eq!(bus.read_long(scrap_info_after_unload + 4), 0);
assert_eq!(bus.read_word(scrap_info_after_unload + 10), 0);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xDEAD_BEEF);
let load = disp.dispatch_toolbox(true, 0x1FB, &mut cpu, &mut bus);
assert!(load.is_some());
assert!(load.unwrap().is_ok());
assert_eq!(bus.read_long(sp), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
let info_after_load = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
assert!(info_after_load.is_some());
assert!(info_after_load.unwrap().is_ok());
let scrap_info_after_load = bus.read_long(sp);
let scrap_handle_after_load = bus.read_long(scrap_info_after_load + 4);
assert_ne!(scrap_handle_after_load, 0);
assert_eq!(bus.read_long(scrap_info_after_load), 12);
assert_eq!(bus.read_word(scrap_info_after_load + 10), 1);
assert_eq!(
bus.get_alloc_size(bus.read_long(scrap_handle_after_load)),
Some(12)
);
}
// Inside Macintosh Volume I, I-458: if the clipboard destination is not
// writable, UnloadScrap can fail without dropping the resident scrap.
#[test]
fn unloadscrap_returns_error_and_keeps_scrap_resident_when_clipboard_unwritable() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
disp.scrap_clipboard_writable = false;
bus.write_long(sp, 0);
let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
assert!(zero.is_some());
assert!(zero.unwrap().is_ok());
let source = bus.alloc(3);
bus.write_bytes(source, b"ABC");
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, source);
bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
bus.write_long(sp + 8, 3);
bus.write_long(sp + 12, 0);
let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
assert!(put.is_some());
assert!(put.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
let info_before = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
assert!(info_before.is_some());
assert!(info_before.unwrap().is_ok());
let scrap_info_before = bus.read_long(sp);
let scrap_handle_before = bus.read_long(scrap_info_before + 4);
assert_ne!(scrap_handle_before, 0);
assert_eq!(bus.read_word(scrap_info_before + 10), 1);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xDEAD_BEEF);
let unload = disp.dispatch_toolbox(true, 0x1FA, &mut cpu, &mut bus);
assert!(unload.is_some());
assert!(unload.unwrap().is_ok());
assert_ne!(bus.read_long(sp), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0);
let info_after_unload = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
assert!(info_after_unload.is_some());
assert!(info_after_unload.unwrap().is_ok());
let scrap_info_after_unload = bus.read_long(sp);
assert_eq!(
bus.read_long(scrap_info_after_unload + 4),
scrap_handle_before
);
assert_eq!(bus.read_word(scrap_info_after_unload + 10), 1);
assert_eq!(bus.read_long(scrap_info_after_unload), 12);
}
// Inside Macintosh Volume I (1985), p. I-458: LoadScrap is a 0-arg Tool
// Trap FUNCTION returning LONGINT (OSStatus) via the Pascal function
// result slot at [SP+0]; the documented "already in memory" success
// path returns noErr without consuming caller stack bytes. Mirrors B1
// of the a9fb_loadscrap_strict catalog bake fixture: pre-poisons the
// 4-byte result slot at SP+0 with a non-zero sentinel and asserts the
// trap overwrites it with 0 (noErr) while leaving A7 untouched.
#[test]
fn loadscrap_writes_noerr_to_pascal_function_result_slot_and_preserves_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0xCAFE_F00D);
bus.write_long(sp + 4, 0xBAAD_F00D);
let result = disp.dispatch_toolbox(true, 0x1FB, &mut cpu, &mut bus);
assert!(result.is_some(), "LoadScrap should be handled");
assert!(result.unwrap().is_ok(), "LoadScrap should return cleanly");
assert_eq!(
bus.read_long(sp),
0,
"LoadScrap should write noErr to the 4-byte Pascal FUNCTION result slot at [SP+0]"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp,
"LoadScrap should leave A7 unchanged (0-arg Tool Trap function)"
);
assert_eq!(
bus.read_long(sp + 4),
0xBAAD_F00D,
"LoadScrap should not write past the 4-byte result slot"
);
}
// ========== Printing Manager ==========
#[test]
fn prglue_selector_param_byte_count_controls_stack_pop() {
// Inside Macintosh Volume V (1986), p. V-408:
// _PrGlue selectors encode parameter-byte count in bits 15-8.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0xF100_0600); // routine=$F1, result=0, params=6
bus.write_long(sp + 4, 0xDEAD_BEEF);
bus.write_word(sp + 8, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
#[test]
fn prglue_propendoc_returns_nil_and_consumes_three_pointer_arguments() {
// Inside Macintosh Volume V (1986), p. V-408:
// PrOpenDoc selector is $04000C00 and signature takes 12 bytes of
// arguments (THPrint, TPPrPort, Ptr) and returns TPPrPort.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0400_0C00);
bus.write_long(sp + 4, 0x1000_2000);
bus.write_long(sp + 8, 0x2000_3000);
bus.write_long(sp + 12, 0x3000_4000);
bus.write_long(sp + 16, 0xFFFF_FFFF); // result slot placeholder
let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 16), 0);
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 16);
}
#[test]
fn prglue_prvalidate_returns_false_boolean_result() {
// Inside Macintosh Volume V (1986), p. V-408:
// PrValidate selector is $52040498 and returns a BOOLEAN.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x5204_0498);
bus.write_long(sp + 4, 0x1234_5678); // hPrint
bus.write_word(sp + 8, 0xFFFF); // result placeholder
let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn prglue_prstldialog_returns_true_boolean_result() {
// Inside Macintosh Volume V (1986), p. V-408:
// PrStlDialog selector is $2A040484 and returns a BOOLEAN.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x2A04_0484);
bus.write_long(sp + 4, 0x1234_5678); // hPrint
bus.write_word(sp + 8, 0); // result placeholder
let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), 1);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn prglue_prjobdialog_returns_true_boolean_result() {
// Inside Macintosh Volume V (1986), p. V-408:
// PrJobDialog selector is $32040488 and returns a BOOLEAN.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x3204_0488);
bus.write_long(sp + 4, 0x1234_5678); // hPrint
bus.write_word(sp + 8, 0); // result placeholder
let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), 1);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn prglue_prclosedoc_consumes_tpprport_argument_without_function_result_slot() {
// Inside Macintosh Volume II (1985), p. II-160; Inside Macintosh
// Volume V (1986), p. V-408:
// PrCloseDoc takes one TPPrPort argument and returns no result.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0800_0484);
bus.write_long(sp + 4, 0x0000_0000); // pPrPort = NIL
bus.write_long(sp + 8, 0xCAFE_BABE); // sentinel past the argument frame
let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
assert_eq!(bus.read_long(sp + 8), 0xCAFE_BABE);
}
#[test]
fn prglue_propen_and_prclose_selector_pops_selector_long() {
// Inside Macintosh Volume II (1985), p. II-151:
// PrOpen and PrClose are procedures with no stack arguments,
// but the raw selector dispatch still consumes the selector
// long pushed for _PrGlue.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0xC800_0000);
let open = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(open.is_some());
assert!(open.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xD000_0000);
let close = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(close.is_some());
assert!(close.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn prglue_propen_and_prclose_preserve_print_error_state() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0xC000_0200);
bus.write_word(sp + 4, 0x1357);
let set_error = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(set_error.is_some());
assert!(set_error.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xC800_0000);
let open = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(open.is_some());
assert!(open.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xD000_0000);
let close = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(close.is_some());
assert!(close.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xBA00_0000);
bus.write_word(sp + 4, 0);
let error = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(error.is_some());
assert!(error.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0x1357);
}
#[test]
fn prglue_prerror_returns_noerr_word_with_zero_result_bits_selector() {
// Inside Macintosh Volume V (1986), p. V-408:
// PrError selector is $BA000000 and returns INTEGER from the
// Printing Manager error state.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0xBA00_0000);
bus.write_word(sp + 4, 0xFFFF); // result placeholder
let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn prglue_prseterror_consumes_ierr_word_without_function_result_slot() {
// Inside Macintosh Volume V (1986), p. V-408:
// PrSetError selector is $C0000200 and takes one INTEGER argument.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0xC000_0200);
bus.write_word(sp + 4, 0xABCD); // iErr
bus.write_word(sp + 6, 0xCAFE); // sentinel after arguments
let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0xCAFE);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
#[test]
fn prglue_prseterror_updates_prerror_state_roundtrip() {
// Inside Macintosh Volume II (1985), p. II-161: PrSetError
// stores the shared PrintErr result code, and PrError returns it.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// First set a non-zero error and verify PrError reports it.
bus.write_long(sp, 0xC000_0200);
bus.write_word(sp + 4, 0x1234);
let set_nonzero = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(set_nonzero.is_some());
assert!(set_nonzero.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xBA00_0000);
bus.write_word(sp + 4, 0xFFFF);
let get_nonzero = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(get_nonzero.is_some());
assert!(get_nonzero.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0x1234);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
// Reset to noErr and verify PrError follows the cleared state.
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xC000_0200);
bus.write_word(sp + 4, 0x0000);
let set_zero = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(set_zero.is_some());
assert!(set_zero.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0xBA00_0000);
bus.write_word(sp + 4, 0xFFFF);
let get_zero = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
assert!(get_zero.is_some());
assert!(get_zero.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0x0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// ========== Utility Traps ==========
#[test]
fn bitshift_large_positive_count_zeroes_on_basiliskii() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 36);
bus.write_long(sp + 2, 1);
bus.write_long(sp + 6, 0xBEEFBEEF);
let result = disp.dispatch_toolbox(true, 0x05C, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 6), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
// Random ($A861) — seed=12345
#[test]
fn test_random() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// globals_ptr is at bus.read_long(A5) = bus.read_long(0x180000) = 0x180004
// randSeed is at globals_ptr - 126 = 0x180004 - 126 = 0x17FF86
let seed_addr = 0x180004u32.wrapping_sub(126);
bus.write_long(seed_addr, 12345);
let result = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// Park-Miller: new_seed = (12345 * 16807) % 2147483647 = 207482415
let new_seed = bus.read_long(seed_addr);
assert_eq!(new_seed, 207482415);
// Result = low 16 bits of new_seed = 207482415 & 0xFFFF = 36399
let result_word = bus.read_word(sp);
assert_eq!(result_word, 207482415u32 as u16); // 36399
// SP unchanged
assert_eq!(cpu.read_reg(Register::A7), sp);
}
// HiWord ($A86A) / LoWord ($A86B)
// IM:I I-472 and OS Utils 1994 p.3-18: extract high/low word from LONGINT.
#[test]
fn hiword_returns_high_order_word_and_pops_two_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x89ABCDEF);
let result = disp.dispatch_toolbox(true, 0x06A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 2), 0x89AB);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
}
#[test]
fn loword_returns_low_order_word_and_pops_two_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x89ABCDEF);
let result = disp.dispatch_toolbox(true, 0x06B, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 2), 0xCDEF);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
}
// Random ($A861)
// IM:I I-194 and OS Utils 1994 p.3-37: result depends solely on randSeed,
// and the pseudorandom sequence is repeatable when randSeed is reset.
#[test]
fn random_reseeding_to_same_value_repeats_sequence_head() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let seed_addr = 0x180004u32.wrapping_sub(126);
bus.write_long(seed_addr, 1);
let first = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
assert!(first.is_some());
assert!(first.unwrap().is_ok());
let first_result = bus.read_word(sp);
let first_seed = bus.read_long(seed_addr);
assert_eq!(first_seed, 16807);
let second = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
assert!(second.is_some());
assert!(second.unwrap().is_ok());
let second_result = bus.read_word(sp);
assert_ne!(second_result, first_result);
bus.write_long(seed_addr, 1);
let replay = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
assert!(replay.is_some());
assert!(replay.unwrap().is_ok());
assert_eq!(bus.read_word(sp), first_result);
assert_eq!(bus.read_long(seed_addr), first_seed);
}
// IM:I I-194 says Random returns -32767..32767 (not -32768).
#[test]
fn random_maps_minus_32768_slot_to_zero() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let seed_addr = 0x180004u32.wrapping_sub(126);
// Chosen so Park-Miller update produces a low word of 0x8000.
bus.write_long(seed_addr, 32768);
let result = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(seed_addr), 550731776);
assert_eq!(bus.read_word(sp), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
}
// NOTE: The former test_pt_to_angle_cardinal_directions test exercised
// a duplicate PtToAngle stub in toolbox.rs that popped 16 bytes and
// treated Rect as an inline record. The canonical PtToAngle now lives
// exclusively in quickdraw.rs (dispatch_quickdraw arm 0x0C3) with the
// correct 12-byte frame (rect passed by pointer). Its contract tests
// are in pttoangle_*.
// GetIndString ($A9E6)
#[test]
fn test_get_ind_string() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let str_ptr = 0x200000u32;
// SP+0: ...(4 bytes), SP+4: str_ptr(4)
bus.write_long(sp, 0); // first 4 bytes (index, resID, etc.)
bus.write_long(sp + 4, str_ptr);
// Write non-zero to str_ptr to verify it gets cleared
bus.write_byte(str_ptr, 0xFF);
let result = disp.dispatch_toolbox(true, 0x1E6, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_byte(str_ptr), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// Inside Macintosh Volume I (1985), p. I-468: GetIndString reads the
// indexed string from a STR# resource via GetResource. A successful hit
// must therefore copy the Pascal-string body and clear stale ResErr.
#[test]
fn get_ind_string_present_resource_copies_indexed_string_and_clears_reserr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let str_ptr = 0x200120u32;
let data_ptr = bus.alloc(16);
// STR# layout: count=2, then PString "ONE", then PString "TWO".
bus.write_word(data_ptr, 2);
bus.write_byte(data_ptr + 2, 3);
bus.write_bytes(data_ptr + 3, b"ONE");
bus.write_byte(data_ptr + 6, 3);
bus.write_bytes(data_ptr + 7, b"TWO");
disp.resources = Some(LoadedResources {
files: HashMap::from([
(0, ResourceFileMap::default()),
(
2,
ResourceFileMap {
loaded: HashMap::from([((*b"STR#", 128i16), data_ptr)]),
named: HashMap::new(),
attrs: HashMap::new(),
map_attrs: 0,
},
),
]),
names: HashMap::new(),
search_order: vec![0, 2],
current_file: 2,
});
bus.write_byte(str_ptr, 0xFF);
bus.write_word(0x0A60, 0xBEEF);
bus.write_word(sp, 2); // index
bus.write_word(sp + 2, 128u16); // STR# id
bus.write_long(sp + 4, str_ptr);
let result = disp.dispatch_toolbox(true, 0x1E6, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_pstring(str_ptr), b"TWO");
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// ========== Sound Manager ==========
// SndPlay ($A805)
#[test]
fn test_snd_play() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// SP+0: async(2), SP+2: sndHdl(4), SP+6: chan(4), SP+10: result(2)
bus.write_word(sp, 0); // async
bus.write_long(sp + 2, 0); // sndHdl (nil = no sound to play)
bus.write_long(sp + 6, 0); // chan
bus.write_word(sp + 10, 0xBEEF); // result placeholder
let result = disp.dispatch_sound(true, 0x005, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 10), (-204i16) as u16); // resProblem
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
#[test]
fn test_snd_play_nil_chan_async_true_reclaims_internal_channel() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let snd_handle = 0x200000u32;
let snd_ptr = 0x200100u32;
// Minimal format 2 'snd ' resource:
// +0 format=2, +2 refCount=0, +4 numCommands=0.
bus.write_long(snd_handle, snd_ptr);
bus.write_word(snd_ptr, 2);
bus.write_word(snd_ptr + 2, 0);
bus.write_word(snd_ptr + 4, 0);
// IM:Sound 1994 p.2-122: if chan is NIL, async is ignored and play
// is synchronous (must not fail solely because async=TRUE).
bus.write_word(sp, 0xFFFF); // async = TRUE
bus.write_long(sp + 2, snd_handle);
bus.write_long(sp + 6, 0); // chan = NIL
bus.write_word(sp + 10, 0xBEEF);
let result = disp.dispatch_sound(true, 0x005, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 10), 0); // noErr
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert!(disp.sound_manager.channels.is_empty());
}
#[test]
fn test_snd_play_unloaded_handle_returns_resproblem() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let snd_handle = 0x200000u32;
// Handle master pointer is NIL => resource is not loaded.
// IM:Sound 1994 p.2-122 says SndPlay returns resProblem (-204).
bus.write_long(snd_handle, 0);
bus.write_word(sp, 0); // async = FALSE
bus.write_long(sp + 2, snd_handle);
bus.write_long(sp + 6, 0);
bus.write_word(sp + 10, 0xBEEF);
let result = disp.dispatch_sound(true, 0x005, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 10), (-204i16) as u16); // resProblem
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert!(disp.sound_manager.channels.is_empty());
}
#[test]
fn test_snd_play_invalid_resource_format_returns_badformat() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let snd_handle = 0x200000u32;
let snd_ptr = 0x200100u32;
bus.write_long(snd_handle, snd_ptr);
bus.write_word(snd_ptr, 3); // unsupported format (valid values: 1 or 2)
bus.write_word(sp, 0); // async = FALSE
bus.write_long(sp + 2, snd_handle);
bus.write_long(sp + 6, 0);
bus.write_word(sp + 10, 0xBEEF);
let result = disp.dispatch_sound(true, 0x005, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// IM:Sound 1994 p.2-123 result codes: malformed/corrupt resource -> badFormat.
assert_eq!(bus.read_word(sp + 10), (-206i16) as u16); // badFormat
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert!(disp.sound_manager.channels.is_empty());
}
// SysBeep ($A9C8)
#[test]
fn test_sys_beep() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 30); // duration param
let result = disp.dispatch_sound(true, 0x1C8, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
}
// SystemClick ($A9B3)
#[test]
fn systemclick_consumes_eventrecord_and_windowptr_arguments() {
// IM:I 1985, p. I-441: SystemClick(theEvent: EventRecord; theWindow: WindowPtr)
// IM:I 1985, p. I-251: EventRecord layout totals 16 bytes.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// 16-byte EventRecord by value + 4-byte WindowPtr.
for i in 0..16u32 {
bus.write_byte(sp + i, (0x40u8).wrapping_add(i as u8));
}
bus.write_long(sp + 16, 0x00AB_CDEF);
bus.write_word(sp + 20, 0xBEEF); // sentinel after argument frame
let result = disp.dispatch_toolbox(true, 0x1B3, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 20);
assert_eq!(
bus.read_word(sp + 20),
0xBEEF,
"SystemClick must pop exactly 20 bytes and not overwrite trailing stack memory"
);
}
#[test]
fn systemclick_repeated_calls_preserve_stack_and_sentinel() {
// Repeating the direct trap call catches cumulative stack drift
// that a single call could miss if the ABI were off by a byte.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
for i in 0..16u32 {
bus.write_byte(sp + i, (0x50u8).wrapping_add(i as u8));
}
bus.write_long(sp + 16, 0x1020_3040);
bus.write_word(sp + 20, 0xC0DE);
for _ in 0..5 {
cpu.write_reg(Register::A7, sp);
let result = disp.dispatch_toolbox(true, 0x1B3, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 20);
assert_eq!(bus.read_word(sp + 20), 0xC0DE);
}
}
// SystemTask ($A9B4)
#[test]
fn systemtask_procedure_call_preserves_stack_pointer() {
// IM:I 1985, p. I-440: PROCEDURE SystemTask;
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_toolbox(true, 0x1B4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"SystemTask is a no-argument procedure and must preserve A7"
);
}
// SystemTask ($A9B4) — mirrors B1 of a9b4_a9c2_systemtask_systemedit_strict
// (5-call composition catches per-call drift a single sandwich might mask).
#[test]
fn systemtask_five_call_composition_preserves_stack_pointer() {
// IM:I 1985, p. I-440: PROCEDURE SystemTask;
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
for _ in 0..5 {
let result = disp.dispatch_toolbox(true, 0x1B4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"SystemTask must preserve A7 across a 5-call composition"
);
}
// OpenDeskAcc ($A9B6)
#[test]
fn opendeskacc_consumes_name_pointer_and_returns_zero_refnum_in_result_slot() {
// IM:I 1985, p. I-440: FUNCTION OpenDeskAcc(theAcc: Str255): INTEGER;
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x240000u32;
bus.write_pstring(name_ptr, b"Calculator");
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 0xBEEF); // INTEGER function-result slot
bus.write_word(sp + 6, 0xCAFE); // trailing sentinel
let result = disp.dispatch_toolbox(true, 0x1B6, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(
bus.read_word(sp + 4),
0,
"OpenDeskAcc HLE path writes 0 into the INTEGER result slot"
);
assert_eq!(
bus.read_word(sp + 6),
0xCAFE,
"OpenDeskAcc must pop only the 4-byte name pointer argument"
);
}
// OpenDeskAcc ($A9B6) — mirrors B1 of a9b6_a9b7_opendeskacc_closedeskacc_strict
// (5-call composition: net stack effect after each Pascal FUNCTION
// call's epilogue is zero, so A7 returns to its pre-composition value).
#[test]
fn opendeskacc_five_call_composition_preserves_stack_pointer() {
// IM:I 1985, p. I-440: FUNCTION OpenDeskAcc(theAcc: Str255): INTEGER;
let (mut disp, mut cpu, mut bus) = setup();
let name_ptr = 0x240000u32;
bus.write_pstring(name_ptr, b"NoSuchDA_A9B6!");
// Each call: caller pushes 2-byte result placeholder + 4-byte name
// ptr, dispatches, trap pops the 4-byte arg + writes the 2-byte
// result slot, then caller pops the 2-byte result slot.
let sp_before = cpu.read_reg(Register::A7);
for _ in 0..5 {
let call_sp = cpu.read_reg(Register::A7);
bus.write_long(call_sp.wrapping_sub(4), name_ptr);
bus.write_word(call_sp.wrapping_sub(6), 0xBEEF);
cpu.write_reg(Register::A7, call_sp.wrapping_sub(6));
let result = disp.dispatch_toolbox(true, 0x1B6, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// Caller's epilogue pops the 2-byte INTEGER result slot.
let post_sp = cpu.read_reg(Register::A7);
cpu.write_reg(Register::A7, post_sp.wrapping_add(2));
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"OpenDeskAcc Pascal FUNCTION calling convention must preserve A7 across a 5-call composition"
);
}
// CloseDeskAcc ($A9B7) — mirrors B2 of a9b6_a9b7_opendeskacc_closedeskacc_strict
#[test]
fn closedeskacc_consumes_refnum_arg_and_writes_no_result() {
// IM:I 1985, p. I-440: PROCEDURE CloseDeskAcc(refNum: INTEGER);
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0); // refNum=0 (clearly invalid — no-op path)
bus.write_word(sp + 2, 0xCAFE); // trailing sentinel
let result = disp.dispatch_toolbox(true, 0x1B7, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(
bus.read_word(sp + 2),
0xCAFE,
"CloseDeskAcc must pop exactly 2 bytes and not overwrite trailing stack memory"
);
}
// CloseDeskAcc ($A9B7) — mirrors B2 of a9b6_a9b7_opendeskacc_closedeskacc_strict
// (5-call composition: each call pops 2 bytes so A7 advances by 10).
#[test]
fn closedeskacc_five_call_composition_advances_stack_by_ten() {
// IM:I 1985, p. I-440: PROCEDURE CloseDeskAcc(refNum: INTEGER);
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
// Caller pre-pushes 5 × 2-byte refNum=0 arguments.
for i in 0..5u32 {
bus.write_word(sp_before.wrapping_sub(10 - 2 * i), 0);
}
cpu.write_reg(Register::A7, sp_before.wrapping_sub(10));
for _ in 0..5 {
let result = disp.dispatch_toolbox(true, 0x1B7, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"CloseDeskAcc must pop exactly 2 bytes per call (Pascal PROCEDURE)"
);
}
// SystemMenu ($A9B5) — mirrors B1 of a9b5_systemmenu_strict (single call).
// Per IM:I 1985, p. I-441: PROCEDURE SystemMenu(menuResult: LONGINT) pops
// a 4-byte LONGINT argument and writes no result slot.
#[test]
fn systemmenu_procedure_call_pops_four_bytes_from_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0); // menuResult=0 (no DA matches menuID=0)
bus.write_word(sp + 4, 0xCAFE); // trailing sentinel
let result = disp.dispatch_toolbox(true, 0x1B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(
bus.read_word(sp + 4),
0xCAFE,
"SystemMenu must pop exactly 4 bytes and not overwrite trailing stack memory"
);
}
// SystemMenu ($A9B5) — mirrors B1 of a9b5_systemmenu_strict (5-call composition).
// Each call pops 4 bytes so A7 advances by 20 after 5 dispatches.
#[test]
fn systemmenu_five_call_composition_advances_stack_by_twenty() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
// Caller pre-pushes 5 × 4-byte LONGINT menuResult=0 arguments.
for i in 0..5u32 {
bus.write_long(sp_before.wrapping_sub(20 - 4 * i), 0);
}
cpu.write_reg(Register::A7, sp_before.wrapping_sub(20));
for _ in 0..5 {
let result = disp.dispatch_toolbox(true, 0x1B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
}
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"SystemMenu must pop exactly 4 bytes per call (Pascal PROCEDURE pop-LONGINT)"
);
}
// SystemEdit ($A9C2)
#[test]
fn systemedit_consumes_editcmd_and_returns_false_boolean_result() {
// IM:I 1985, p. I-441:
// FUNCTION SystemEdit(editCmd: INTEGER): BOOLEAN;
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 3); // copyCmd
bus.write_word(sp + 2, 0xFFFF); // BOOLEAN result slot sentinel
bus.write_word(sp + 4, 0xCAFE); // trailing sentinel
let result = disp.dispatch_toolbox(true, 0x1C2, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(
bus.read_word(sp + 2),
0,
"SystemEdit should return FALSE when no desk accessory handles edit commands"
);
assert_eq!(
bus.read_word(sp + 4),
0xCAFE,
"SystemEdit must pop only the 2-byte editCmd argument"
);
}
// SystemEdit ($A9C2) — mirrors B2 of a9b4_a9c2_systemtask_systemedit_strict
// (FALSE return + 2-byte arg pop for every standard editCmd per IM:I I-441).
#[test]
fn systemedit_returns_false_for_every_standard_editcmd() {
// IM:I 1985, p. I-441 table:
// 0 undoCmd
// 2 cutCmd
// 3 copyCmd
// 4 pasteCmd
// 5 clearCmd
// (1 is a historic gap.)
for edit_cmd in [0u16, 2, 3, 4, 5] {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, edit_cmd);
bus.write_word(sp + 2, 0xFFFF); // BOOLEAN result slot sentinel
bus.write_word(sp + 4, 0xCAFE); // trailing sentinel
let result = disp.dispatch_toolbox(true, 0x1C2, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
sp + 2,
"SystemEdit(editCmd={edit_cmd}) must pop exactly 2 bytes"
);
assert_eq!(
bus.read_word(sp + 2),
0,
"SystemEdit(editCmd={edit_cmd}) must return FALSE in the no-DA path"
);
assert_eq!(
bus.read_word(sp + 4),
0xCAFE,
"SystemEdit(editCmd={edit_cmd}) must not overwrite trailing stack"
);
}
}
fn seed_synthetic_kchr(bus: &mut super::MacMemoryBus, trans_data: u32) {
// Minimal KCHR layout:
// byte 0 = version
// bytes 1..=256 = table-selection index
// table 0 and table 1 = 128-byte character tables
//
// Modifier byte 0 selects table 0; modifier byte 1 selects table 1.
bus.write_byte(trans_data, 0);
for i in 0..256u32 {
bus.write_byte(trans_data + 1 + i, 0);
}
bus.write_byte(trans_data + 1, 0);
bus.write_byte(trans_data + 2, 1);
let table0 = trans_data + 1 + 256;
let table1 = table0 + 128;
bus.write_byte(table0 + 2, b'Q');
bus.write_byte(table0 + 3, b'W');
bus.write_byte(table0 + 4, b'E');
bus.write_byte(table0 + 5, b'R');
bus.write_byte(table0 + 6, b'T');
bus.write_byte(table1 + 2, b'Z');
bus.write_byte(table1 + 3, b'X');
bus.write_byte(table1 + 4, b'C');
bus.write_byte(table1 + 5, b'V');
bus.write_byte(table1 + 6, b'B');
}
// KeyTrans ($A9C3)
#[test]
fn keytrans_consumes_state_keycode_transdata_arguments_and_writes_long_result_slot() {
// Inside Macintosh: Macintosh Toolbox Essentials (1992), p. 2-110:
// FUNCTION KeyTranslate(transData: Ptr; keycode: Integer;
// VAR state: LongInt): LongInt;
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let state_ptr = 0x230000u32;
let trans_data = 0x240000u32;
seed_synthetic_kchr(&mut bus, trans_data);
bus.write_long(state_ptr, 0x1234_5678);
bus.write_long(sp, state_ptr);
bus.write_word(sp + 4, 0x0002); // virtual keycode 2
bus.write_long(sp + 6, trans_data);
bus.write_long(sp + 10, 0xDEAD_BEEF); // result slot sentinel
bus.write_word(sp + 14, 0xCAFE); // trailing sentinel
let result = disp.dispatch_toolbox(true, 0x1C3, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(
bus.read_long(sp + 10),
0x0000_0051,
"KeyTrans should write the translated character at the result slot"
);
assert_eq!(
bus.read_word(sp + 14),
0xCAFE,
"KeyTrans must pop exactly state/keycode/transData arguments (10 bytes)"
);
assert_eq!(
bus.read_long(state_ptr),
0,
"KeyTrans should clear pending state on the nominal path"
);
}
#[test]
fn keytrans_single_character_result_uses_charcode2_low_byte() {
// Inside Macintosh: Macintosh Toolbox Essentials (1992), p. 2-111:
// when one character is returned, it is in Character code 2
// (low byte) of the 32-bit result.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let state_ptr = 0x230100u32;
let trans_data = 0x240100u32;
seed_synthetic_kchr(&mut bus, trans_data);
bus.write_long(state_ptr, 0);
bus.write_long(sp, state_ptr);
bus.write_word(sp + 4, 0x0102); // modifier byte 1, keycode 2
bus.write_long(sp + 6, trans_data);
bus.write_long(sp + 10, 0);
let result = disp.dispatch_toolbox(true, 0x1C3, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let translated = bus.read_long(sp + 10);
assert_eq!(
translated & 0x0000_00FF,
0x5A,
"single-character output should occupy Character code 2 (low byte)"
);
assert_eq!(
translated & 0x00FF_0000,
0,
"Character code 1 byte should be zero when only one character is returned"
);
}
#[test]
fn keytrans_non_deadkey_path_clears_state_for_followup_calls() {
// Inside Macintosh: Text (1993), C-19..C-20: state carries dead-key
// context; nominal non-dead-key translation should leave no pending
// state for the next call.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let state_ptr = 0x230200u32;
let trans_data = 0x240200u32;
seed_synthetic_kchr(&mut bus, trans_data);
bus.write_long(state_ptr, 0xFFFF_0001); // stale nonzero value
bus.write_long(sp, state_ptr);
bus.write_word(sp + 4, 0x0003); // virtual keycode 3
bus.write_long(sp + 6, trans_data);
bus.write_long(sp + 10, 0);
let result = disp.dispatch_toolbox(true, 0x1C3, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_long(state_ptr),
0,
"KeyTrans nominal HLE path should clear pending dead-key state"
);
assert_eq!(
bus.read_long(sp + 10),
0x0000_0057,
"KeyTrans should still return the translated character"
);
}
// GetAppParms ($A9F5)
#[test]
fn getappparms_returns_curapname_curaprefnum_and_appparmhandle() {
// IM:II 1985, p. II-58: GetAppParms returns CurApName, CurApRefNum,
// and AppParmHandle through its three VAR output parameters.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let ap_param_out = 0x210000u32;
let ap_refnum_out = 0x210100u32;
let ap_name_out = 0x210200u32;
bus.write_pstring(0x0910, b"Marathon");
bus.write_word(0x0900, (-6i16) as u16);
bus.write_long(0x0AEC, 0x00AB_CDEF);
bus.write_long(ap_param_out, 0xDEAD_BEEF);
bus.write_word(ap_refnum_out, 0xBEEF);
bus.write_pstring(ap_name_out, b"XXXX");
bus.write_long(sp, ap_param_out);
bus.write_long(sp + 4, ap_refnum_out);
bus.write_long(sp + 8, ap_name_out);
let result = disp.dispatch_toolbox(true, 0x1F5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
assert_eq!(bus.read_pstring(ap_name_out), b"Marathon".to_vec());
assert_eq!(bus.read_word(ap_refnum_out), (-6i16) as u16);
assert_eq!(bus.read_long(ap_param_out), 0x00AB_CDEF);
}
#[test]
fn getappparms_consumes_three_var_pointer_arguments() {
// IM:II 1985, p. II-58 signature:
// GetAppParms(VAR Str255, VAR INTEGER, VAR Handle) -> 3 pointers.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0);
bus.write_long(sp + 4, 0);
bus.write_long(sp + 8, 0);
let result = disp.dispatch_toolbox(true, 0x1F5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
#[test]
fn unloadseg_consumes_routineaddr_pointer_argument() {
// IM:II 1985, p. II-58:
// PROCEDURE UnloadSeg(routineAddr: Ptr);
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x00AB_CDEF);
bus.write_word(sp + 4, 0xBEEF); // sentinel after pointer argument
let result = disp.dispatch_toolbox(true, 0x1F1, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(sp + 4), 0xBEEF);
}
#[test]
fn unloadseg_noop_preserves_registered_segment_cache() {
// Systemless's HLE keeps segment_map resident; UnloadSeg currently
// contracts to pointer-pop + no mutation of registered segments.
let (mut disp, mut cpu, mut bus) = setup();
let seg_map = HashMap::from([(1i16, 0x0022_0000u32), (9i16, 0x0033_0000u32)]);
disp.register_segments(seg_map.clone());
let sp = TEST_SP;
bus.write_long(sp, 0x0022_1000);
let result = disp.dispatch_toolbox(true, 0x1F1, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(disp.segment_map, seg_map);
}
// ExitToShell ($A9F4)
#[test]
fn exittoshell_terminates_application_and_returns_halted_error() {
// IM:II 1985, p. II-58: ExitToShell terminates the current app
// and returns to the shell.
let (mut disp, mut cpu, mut bus) = setup();
let result = disp.dispatch_toolbox(true, 0x1F4, &mut cpu, &mut bus);
assert!(result.is_some());
let err = result.unwrap().unwrap_err();
assert!(
matches!(err, crate::Error::Halted),
"ExitToShell should return Error::Halted"
);
}
#[test]
fn exittoshell_procedure_signature_consumes_no_stack_arguments() {
// IM:II 1985, p. II-58: PROCEDURE ExitToShell; (no arguments).
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_toolbox(true, 0x1F4, &mut cpu, &mut bus);
assert!(result.is_some());
let err = result.unwrap().unwrap_err();
assert!(matches!(err, crate::Error::Halted));
assert_eq!(cpu.read_reg(Register::A7), sp_before);
}
#[test]
fn exittoshell_halted_path_preserves_d0_and_a7() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0x1234_5678);
let result = disp.dispatch_toolbox(true, 0x1F4, &mut cpu, &mut bus);
assert!(result.is_some(), "ExitToShell should be handled");
assert!(
matches!(result.unwrap().unwrap_err(), crate::Error::Halted),
"ExitToShell should return Error::Halted"
);
assert_eq!(cpu.read_reg(Register::D0), 0x1234_5678);
assert_eq!(cpu.read_reg(Register::A7), sp_before);
}
#[test]
fn launchapplication_launchcontinue_clear_records_target_app_path_and_halts() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let launch_pb = bus.alloc(64);
let app_spec = bus.alloc(32);
let target_dir_id = disp.ensure_vfs_directory("LaunchTargets");
cpu.write_reg(Register::A0, launch_pb);
cpu.write_reg(Register::D0, 0x1234_5678);
for offset in 0..64u32 {
bus.write_byte(launch_pb + offset, 0);
}
bus.write_word(launch_pb + 6, 0x4C43); // extendedBlock
bus.write_long(launch_pb + 8, 32); // extendedBlockLen
bus.write_word(launch_pb + 12, 0);
bus.write_word(launch_pb + 14, 0);
bus.write_long(launch_pb + 16, app_spec);
for offset in 0..32u32 {
bus.write_byte(app_spec + offset, 0);
}
bus.write_word(app_spec, 0);
bus.write_long(app_spec + 2, target_dir_id);
write_pascal_string(&mut bus, app_spec + 6, "NoSuchApp");
let result = disp.dispatch_toolbox(true, 0x1F2, &mut cpu, &mut bus);
assert!(result.is_some(), "LaunchApplication should be handled");
assert!(
matches!(result.unwrap().unwrap_err(), crate::Error::Halted),
"LaunchApplication should return Error::Halted"
);
assert_eq!(cpu.read_reg(Register::A0), launch_pb);
assert_eq!(cpu.read_reg(Register::D0) as i32, -43);
assert_eq!(cpu.read_reg(Register::A7), sp_before);
assert_eq!(bus.read_long(launch_pb + 20), 0);
assert_eq!(bus.read_long(launch_pb + 24), 0);
assert_eq!(bus.read_long(launch_pb + 28), 0);
assert_eq!(bus.read_long(launch_pb + 32), 0);
assert_eq!(bus.read_long(launch_pb + 36), 0);
assert_eq!(
disp.launched_app_path.as_deref(),
Some("LaunchTargets/NoSuchApp")
);
assert_eq!(disp.default_dir_id, target_dir_id);
assert_ne!(disp.app_wd_refnum, 0);
}
#[test]
fn launchapplication_launchcontinue_set_records_target_app_path_and_returns() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let launch_pb = bus.alloc(64);
let app_spec = bus.alloc(32);
let target_dir_id = disp.ensure_vfs_directory("LaunchTargets");
cpu.write_reg(Register::A0, launch_pb);
cpu.write_reg(Register::D0, 0x1234_5678);
for offset in 0..64u32 {
bus.write_byte(launch_pb + offset, 0);
}
bus.write_word(launch_pb + 6, 0x4C43); // extendedBlock
bus.write_long(launch_pb + 8, 32); // extendedBlockLen
bus.write_word(launch_pb + 12, 0);
bus.write_word(launch_pb + 14, 0x4000); // launchContinue
bus.write_long(launch_pb + 16, app_spec);
for offset in 0..32u32 {
bus.write_byte(app_spec + offset, 0);
}
bus.write_word(app_spec, 0);
bus.write_long(app_spec + 2, target_dir_id);
write_pascal_string(&mut bus, app_spec + 6, "NoSuchApp");
let result = disp.dispatch_toolbox(true, 0x1F2, &mut cpu, &mut bus);
assert!(result.is_some(), "LaunchApplication should be handled");
assert!(
result.unwrap().is_ok(),
"LaunchApplication should return when launchContinue is set"
);
assert_eq!(cpu.read_reg(Register::A0), launch_pb);
assert_eq!(cpu.read_reg(Register::D0) as i32, -43);
assert_eq!(cpu.read_reg(Register::A7), sp_before);
assert_eq!(bus.read_long(launch_pb + 20), 0);
assert_eq!(bus.read_long(launch_pb + 24), 0);
assert_eq!(bus.read_long(launch_pb + 28), 0);
assert_eq!(bus.read_long(launch_pb + 32), 0);
assert_eq!(bus.read_long(launch_pb + 36), 0);
assert_eq!(
disp.launched_app_path.as_deref(),
Some("LaunchTargets/NoSuchApp")
);
assert_eq!(disp.default_dir_id, target_dir_id);
assert_ne!(disp.app_wd_refnum, 0);
}
#[test]
fn chain_records_cmdline_path_and_curpageoption_before_halt() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let cmd_line = bus.alloc(8);
let app_name = bus.alloc(32);
let target_dir_id = disp.ensure_vfs_directory("ChainTargets");
disp.default_dir_id = target_dir_id;
cpu.write_reg(Register::A0, cmd_line);
cpu.write_reg(Register::D0, 0x1234_5678);
for offset in 0..8u32 {
bus.write_byte(cmd_line + offset, 0);
}
bus.write_long(cmd_line, app_name);
bus.write_word(cmd_line + 4, 0x0001);
write_pascal_string(&mut bus, app_name, "NoSuchApp");
let result = disp.dispatch_toolbox(true, 0x1F3, &mut cpu, &mut bus);
assert!(result.is_some(), "Chain should be handled");
assert!(
matches!(result.unwrap().unwrap_err(), crate::Error::Halted),
"Chain should return Error::Halted"
);
assert_eq!(cpu.read_reg(Register::A0), cmd_line);
assert_eq!(cpu.read_reg(Register::D0), 0x1234_5678);
assert_eq!(cpu.read_reg(Register::A7), sp_before);
assert_eq!(bus.read_word(0x0936), 0x0001);
assert_eq!(
disp.launched_app_path.as_deref(),
Some("ChainTargets/NoSuchApp")
);
assert_eq!(disp.default_dir_id, target_dir_id);
assert_ne!(disp.app_wd_refnum, 0);
}
// _SCSIDispatch ($A815)
#[test]
fn scsidispatch_selector_zero_returns_noerr_and_pops_selector_word() {
// Inside Macintosh Volume IV (1986), pp. IV-287 to IV-300:
// selector 0 (SCSIReset) is a word-selector dispatch entry.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0x1234_5678);
bus.write_word(sp_before + 2, 0xFFFF);
bus.write_word(sp_before, 0);
let result = disp.dispatch_toolbox(true, 0x015, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp_before + 2);
assert_eq!(bus.read_word(sp_before + 2), 0);
assert_eq!(cpu.read_reg(Register::D0), 0x1234_5678);
}
#[test]
fn scsidispatch_selector_two_returns_noerr_and_pops_two_byte_argument_frame() {
// Inside Macintosh Volume IV (1986), pp. IV-287 to IV-300:
// selector 2 (SCSISelect) consumes its selector word plus one
// 2-byte argument before the OSErr result slot.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0x89AB_CDEF);
bus.write_word(sp_before + 4, 0xFFFF);
bus.write_word(sp_before, 2);
bus.write_word(sp_before + 2, 0x1357);
let result = disp.dispatch_toolbox(true, 0x015, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
assert_eq!(bus.read_word(sp_before + 4), 0);
assert_eq!(cpu.read_reg(Register::D0), 0x89AB_CDEF);
}
#[test]
fn scsidispatch_selector_three_returns_noerr_and_pops_six_byte_argument_frame() {
// Inside Macintosh Volume IV (1986), pp. IV-287 to IV-300:
// selector 3 (SCSICmd) consumes three 2-byte arguments.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0x0BAD_F00D);
bus.write_word(sp_before + 8, 0xFFFF);
bus.write_word(sp_before, 3);
bus.write_word(sp_before + 2, 0x1111);
bus.write_word(sp_before + 4, 0x2222);
bus.write_word(sp_before + 6, 0x3333);
let result = disp.dispatch_toolbox(true, 0x015, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
assert_eq!(bus.read_word(sp_before + 8), 0);
assert_eq!(cpu.read_reg(Register::D0), 0x0BAD_F00D);
}
// Debugger ($A9FF)
#[test]
fn debugger_trap_is_parameterless_and_preserves_stack_pointer() {
// Universal Interfaces Types.h declares Debugger() as a
// parameterless one-word inline trap.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_toolbox(true, 0x1FF, &mut cpu, &mut bus);
assert!(result.is_some(), "Debugger should be handled");
assert!(result.unwrap().is_ok(), "Debugger should return");
assert_eq!(cpu.read_reg(Register::A7), sp_before);
}
#[test]
fn debugger_without_installed_debugger_returns_to_caller() {
// On a stock System 7 setup without MacsBug installed,
// Debugger returns immediately to the caller.
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D0, 0x1234_5678);
cpu.write_reg(Register::A0, 0x00AA_5500);
let result = disp.dispatch_toolbox(true, 0x1FF, &mut cpu, &mut bus);
assert!(result.is_some(), "Debugger should be handled");
assert!(result.unwrap().is_ok(), "Debugger should return to caller");
assert_eq!(cpu.read_reg(Register::D0), 0x1234_5678);
assert_eq!(cpu.read_reg(Register::A0), 0x00AA_5500);
}
// ShutDwnPower ($A895)
#[test]
fn test_shut_dwn_power() {
let (mut disp, mut cpu, mut bus) = setup();
let result = disp.dispatch_toolbox(true, 0x095, &mut cpu, &mut bus);
assert!(result.is_some());
let err = result.unwrap().unwrap_err();
assert!(
matches!(err, crate::Error::Halted),
"ShutDwnPower should return Error::Halted"
);
}
// ShutDwnInstall ($A895 selector 3)
// Inside Macintosh Volume V, V-589.
// Universal Headers <ShutDown.h> declares the call as
// THREEWORDINLINE(0x3F3C, 0x0003, 0xA895)
// — the compiler emits MOVE.W #3,-(A7) immediately before $A895, so
// SP at trap entry holds selector(2) + flags(2) + proc(4) = 8 bytes.
#[test]
fn shutdwninstall_pops_eight_byte_argument_frame_and_returns() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_at_trap = TEST_SP;
bus.write_word(sp_at_trap, 3); // selector
bus.write_word(sp_at_trap + 2, 0x0001); // flags = sdOnPowerOff
bus.write_long(sp_at_trap + 4, 0xCAFE_BABE); // proc (never invoked)
cpu.write_reg(Register::A7, sp_at_trap);
let result = disp.dispatch_toolbox(true, 0x095, &mut cpu, &mut bus);
let inner = result.expect("_Shutdown must be a handled trap");
assert!(
inner.is_ok(),
"ShutDwnInstall (selector 3) must return Ok, got {:?}",
inner
);
assert_eq!(
cpu.read_reg(Register::D0),
0,
"sdInstall must report noErr in D0"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp_at_trap + 8,
"sdInstall must pop selector(2) + flags(2) + proc(4) = 8 bytes"
);
}
// ShutDwnRemove ($A895 selector 4)
// Inside Macintosh Volume V, V-590.
// Universal Headers <ShutDown.h> declares the call as
// THREEWORDINLINE(0x3F3C, 0x0004, 0xA895)
// — compiler emits MOVE.W #4,-(A7) immediately before $A895, so SP at
// trap entry holds selector(2) + proc(4) = 6 bytes.
#[test]
fn shutdwnremove_pops_six_byte_argument_frame_and_returns() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_at_trap = TEST_SP;
bus.write_word(sp_at_trap, 4); // selector
bus.write_long(sp_at_trap + 2, 0xCAFE_BABE); // proc (never invoked)
cpu.write_reg(Register::A7, sp_at_trap);
let result = disp.dispatch_toolbox(true, 0x095, &mut cpu, &mut bus);
let inner = result.expect("_Shutdown must be a handled trap");
assert!(
inner.is_ok(),
"ShutDwnRemove (selector 4) must return Ok, got {:?}",
inner
);
assert_eq!(
cpu.read_reg(Register::D0),
0,
"sdRemove must report noErr in D0"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp_at_trap + 6,
"sdRemove must pop selector(2) + proc(4) = 6 bytes"
);
}
// SetFractEnable ($A814)
//
// Per Inside Macintosh Volume IV (1986), p. IV-32, SetFractEnable
// writes the BOOLEAN argument byte verbatim into the FractEnable
// low-memory global at $0BF4. MPW Pascal BOOLEAN convention places
// the value byte in the HIGH byte of the 2-byte stack slot, so the
// trap reads byte at SP+0 (not SP+1) and writes that byte to $0BF4
// unchanged — TRUE → 0x01, FALSE → 0x00 (NOT a normalised 0xFF for
// TRUE). These tests pre-poison $0BF4 with a distinct sentinel and
// assert (a) exact byte parity with the input high-byte, (b) the
// trap pops exactly 2 bytes (Pascal PROCEDURE protocol — no
// function-result slot), and (c) memory adjacent to $0BF4 is
// preserved (regression guard against any future "fix" that writes
// a wider word or normalises the byte).
#[test]
fn setfractenable_true_writes_one_byte_verbatim_to_fract_enable_global() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// Pre-poison $0BF4 with a sentinel and the adjacent bytes with
// distinct values to catch any over-write.
bus.write_byte(0x0BF4, 0xA5);
bus.write_byte(0x0BF3, 0x77);
bus.write_byte(0x0BF5, 0x88);
// Push BOOLEAN TRUE: value byte 0x01 in the HIGH byte of the
// 2-byte stack slot.
bus.write_byte(sp, 0x01);
bus.write_byte(sp + 1, 0x00);
let result = disp.dispatch_toolbox(true, 0x014, &mut cpu, &mut bus);
assert!(result.is_some(), "SetFractEnable must be a handled trap");
assert!(result.unwrap().is_ok());
// FractEnable byte at $0BF4 holds 0x01 — verbatim BOOLEAN high byte.
assert_eq!(bus.read_byte(0x0BF4), 0x01);
// Adjacent bytes preserved — no over-write past the single byte.
assert_eq!(bus.read_byte(0x0BF3), 0x77);
assert_eq!(bus.read_byte(0x0BF5), 0x88);
// A7 advanced by exactly 2 — Pascal PROCEDURE pops the BOOLEAN arg.
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
}
#[test]
fn setfractenable_false_writes_zero_byte_to_fract_enable_global() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// Pre-poison with a non-zero value so we can detect that the
// trap actually wrote 0x00 (rather than no-op).
bus.write_byte(0x0BF4, 0xC3);
bus.write_byte(sp, 0x00);
bus.write_byte(sp + 1, 0x00);
let result = disp.dispatch_toolbox(true, 0x014, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
// FractEnable byte at $0BF4 cleared to 0x00.
assert_eq!(bus.read_byte(0x0BF4), 0x00);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
}
#[test]
fn setfractenable_consumes_two_byte_boolean_argument_and_balances_stack() {
// Pascal PROCEDURE protocol: caller pushes one 2-byte BOOLEAN,
// trap pops 2 bytes, no function-result slot. Net externally
// observed A7 movement is +2 (the trap's pop). This test asserts
// the exact pop count regardless of the BOOLEAN value to defeat
// any future change that pops a different number of bytes
// (e.g. 4 for a hypothetical LONGINT widening).
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_byte(sp, 0x01);
bus.write_byte(sp + 1, 0x00);
let result = disp.dispatch_toolbox(true, 0x014, &mut cpu, &mut bus);
assert!(result.unwrap().is_ok());
assert_eq!(
cpu.read_reg(Register::A7),
sp + 2,
"SetFractEnable must pop exactly the 2-byte BOOLEAN argument"
);
}
// Delay ($A03B) — OS trap
#[test]
fn test_delay() {
let (mut disp, mut cpu, mut bus) = setup();
// A0 = numTicks (0 from setup), Ticks at $016A = 100 (from setup)
// finalTicks = 100 + 0 = 100
let result = disp.dispatch_toolbox(false, 0x3B, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::D0), 100); // finalTicks returned in D0
}
// ========== Sound Manager (extended) ==========
// SoundDispatch ($A800)
// Selector encoding: bits 31-24 = param_bytes/2, bits 23-16 = routine
// Sound 1994, 2-256
#[test]
fn test_sound_dispatch() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// Selector goes in D0, not on the stack.
// 0x00080008: param_bytes=0, routine=$08 (unknown → stub)
cpu.write_reg(Register::D0, 0x00080008);
let result = disp.dispatch_sound(true, 0x000, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// No params to pop, SP unchanged
assert_eq!(cpu.read_reg(Register::A7), sp);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// SndNewChannel ($A807)
#[test]
fn test_snd_new_channel() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let chan_ptr_ptr = 0x300000u32;
// Pascal stack (right-to-left push):
// SP+0: userRoutine (4, ProcPtr)
// SP+4: init (4, LongInt)
// SP+8: synth (2, Integer)
// SP+10: chan ptr (4, VAR SndChannelPtr)
// SP+14: result (2, OSErr)
// Sound 1994, 2-195
bus.write_long(sp, 0); // userRoutine
bus.write_long(sp + 4, 0); // init
bus.write_word(sp + 8, 0); // synth
bus.write_long(sp + 10, chan_ptr_ptr); // chan_ptr_ptr
bus.write_word(sp + 14, 0xBEEF); // result placeholder
let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 14), 0); // noErr
assert_eq!(cpu.read_reg(Register::A7), sp + 14);
// Verify a channel was allocated (chan_ptr_ptr should point to non-zero)
let chan_ptr = bus.read_long(chan_ptr_ptr);
assert_ne!(chan_ptr, 0, "SndNewChannel should allocate a channel");
}
#[test]
fn test_snd_new_channel_writes_callback_procptr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let chan_ptr_ptr = 0x300000u32;
let user_routine = 0x00AB_CDEFu32;
// Per IM:Sound 1994 p.2-195, SndNewChannel installs the caller's
// callback procedure for callBackCmd processing.
bus.write_long(sp, user_routine); // userRoutine
bus.write_long(sp + 4, 0); // init
bus.write_word(sp + 8, 0); // synth
bus.write_long(sp + 10, chan_ptr_ptr); // chan VAR pointer
bus.write_word(sp + 14, 0xBEEF); // result placeholder
let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let chan_ptr = bus.read_long(chan_ptr_ptr);
assert_ne!(chan_ptr, 0);
// Sound channel callback field is at offset +8 in SndChannel.
assert_eq!(bus.read_long(chan_ptr + 8), user_routine);
assert_eq!(disp.sound_manager.channels.len(), 1);
assert_eq!(disp.sound_manager.channels[0].callback_addr, user_routine);
}
// SndDisposeChannel ($A801)
#[test]
fn test_snd_dispose_channel() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// SP+0: quiet(2), SP+2: chan(4), SP+6: result(2)
bus.write_word(sp, 0);
bus.write_long(sp + 2, 0);
bus.write_word(sp + 6, 0xBEEF);
let result = disp.dispatch_sound(true, 0x001, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
#[test]
fn test_snd_dispose_channel_removes_allocated_channel() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let chan_ptr_ptr = 0x300000u32;
// Allocate channel first (IM:Sound 1994 p.2-195).
bus.write_long(sp, 0); // userRoutine
bus.write_long(sp + 4, 0); // init
bus.write_word(sp + 8, 0); // synth
bus.write_long(sp + 10, chan_ptr_ptr); // chan VAR pointer
bus.write_word(sp + 14, 0xBEEF); // result placeholder
let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let chan_ptr = bus.read_long(chan_ptr_ptr);
assert_ne!(chan_ptr, 0);
assert_eq!(disp.sound_manager.channels.len(), 1);
// Dispose the channel (IM:Sound 1994 p.2-196).
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1); // quietNow = TRUE
bus.write_long(sp + 2, chan_ptr);
bus.write_word(sp + 6, 0xBEEF);
let result = disp.dispatch_sound(true, 0x001, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
assert!(disp.sound_manager.channels.is_empty());
}
// SndDoCommand ($A803)
#[test]
fn test_snd_do_command() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// SP+0: noWait(2), SP+2: cmd(4, ptr to SndCommand), SP+6: chan(4), SP+10: result(2)
// Sound 1994, 2-130
let cmd_addr = 0x200100u32;
bus.write_word(cmd_addr, 0); // cmd = nullCmd
bus.write_word(cmd_addr + 2, 0); // param1
bus.write_long(cmd_addr + 4, 0); // param2
bus.write_word(sp, 0); // noWait
bus.write_long(sp + 2, cmd_addr); // cmd ptr
bus.write_long(sp + 6, 0); // chan
bus.write_word(sp + 10, 0xBEEF); // result placeholder
let result = disp.dispatch_sound(true, 0x003, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 10), (-205i16) as u16);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
#[test]
fn test_snd_do_command_callback_cmd_queues_pending_callback() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let chan_ptr_ptr = 0x300000u32;
let user_routine = 0x00AB_CDEFu32;
let cmd_addr = 0x200100u32;
// Install channel callback via SndNewChannel (IM:Sound 1994 p.2-195).
bus.write_long(sp, user_routine); // userRoutine
bus.write_long(sp + 4, 0); // init
bus.write_word(sp + 8, 0); // synth
bus.write_long(sp + 10, chan_ptr_ptr); // chan VAR pointer
bus.write_word(sp + 14, 0xBEEF);
let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let chan_ptr = bus.read_long(chan_ptr_ptr);
assert_ne!(chan_ptr, 0);
// callBackCmd should schedule the channel callback procedure.
// IM:Sound notes to issue callBackCmd with SndDoCommand (not
// SndDoImmediate) so queue ordering is preserved.
cpu.write_reg(Register::A7, sp);
bus.write_word(cmd_addr, crate::sound::cmd::CALLBACK);
bus.write_word(cmd_addr + 2, 7);
bus.write_long(cmd_addr + 4, 0x1111_2222);
bus.write_word(sp, 0); // noWait
bus.write_long(sp + 2, cmd_addr);
bus.write_long(sp + 6, chan_ptr);
bus.write_word(sp + 10, 0xBEEF);
let result = disp.dispatch_sound(true, 0x003, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 10), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(disp.sound_manager.pending_sound_callbacks.len(), 1);
match &disp.sound_manager.pending_sound_callbacks[0] {
crate::sound::PendingSoundCallback::Command {
callback_addr,
chan_ptr: queued_chan_ptr,
cmd,
} => {
assert_eq!(*callback_addr, user_routine);
assert_eq!(*queued_chan_ptr, chan_ptr);
assert_eq!(cmd.cmd, crate::sound::cmd::CALLBACK);
assert_eq!(cmd.param1, 7);
assert_eq!(cmd.param2, 0x1111_2222);
}
other => panic!("expected command callback, got {other:?}"),
}
}
// SndDoImmediate ($A804)
#[test]
fn test_snd_do_immediate() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// SP+0: cmd(4, ptr to SndCommand), SP+4: chan(4), SP+8: result(2)
// Sound 1994, 2-131
let cmd_addr = 0x200100u32;
bus.write_word(cmd_addr, 0); // cmd = nullCmd
bus.write_word(cmd_addr + 2, 0); // param1
bus.write_long(cmd_addr + 4, 0); // param2
bus.write_long(sp, cmd_addr); // cmd ptr
bus.write_long(sp + 4, 0); // chan
bus.write_word(sp + 8, 0xBEEF); // result placeholder
let result = disp.dispatch_sound(true, 0x004, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), (-205i16) as u16);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
#[test]
fn test_snd_do_immediate_get_rate_writes_unity_fixed() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let chan_ptr_ptr = 0x300000u32;
let cmd_addr = 0x200100u32;
let rate_out_addr = 0x200200u32;
// Allocate a channel first so getRateCmd queries a real channel.
bus.write_long(sp, 0); // userRoutine
bus.write_long(sp + 4, 0); // init
bus.write_word(sp + 8, 0); // synth
bus.write_long(sp + 10, chan_ptr_ptr); // chan VAR pointer
bus.write_word(sp + 14, 0xBEEF);
let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let chan_ptr = bus.read_long(chan_ptr_ptr);
assert_ne!(chan_ptr, 0);
// IM:Sound documents getRateCmd writes the channel rate as a Fixed
// through param2 when SndDoImmediate returns noErr.
cpu.write_reg(Register::A7, sp);
bus.write_word(cmd_addr, crate::sound::cmd::GET_RATE);
bus.write_word(cmd_addr + 2, 0);
bus.write_long(cmd_addr + 4, rate_out_addr);
bus.write_long(rate_out_addr, 0xDEAD_BEEFu32);
bus.write_long(sp, cmd_addr);
bus.write_long(sp + 4, chan_ptr);
bus.write_word(sp + 8, 0xBEEF);
let result = disp.dispatch_sound(true, 0x004, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), 0); // noErr
assert_eq!(bus.read_long(rate_out_addr), 0x0001_0000); // unity rate
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// ========== Resource Manager (extended) ==========
// OpenRFPerm ($A9C4) — file not found
#[test]
fn test_open_rf_perm_not_found() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// SP+0: perm(2), SP+2: vref(2), SP+4: name_ptr(4), SP+8: result(2)
let name_ptr = 0x200000u32;
bus.write_word(sp, 1); // perm = fsRdPerm
bus.write_word(sp + 2, 0); // vRefNum
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0x0000); // result placeholder
// Write Pascal string filename at name_ptr
bus.write_pstring(name_ptr, b"NoSuchFile");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// result = -1 as u16 = 0xFFFF
assert_eq!(bus.read_word(sp + 8), (-1i16) as u16);
// D0 mirrors the FUNCTION result slot (-1 on failure).
assert_eq!(cpu.read_reg(Register::D0), (-1i32) as u32);
// ResErr at $0A60 = -43
assert_eq!(bus.read_word(0x0A60), (-43i16) as u16);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// IM:IV IV-17 and MTb 1993 1-64..1-66: successful OpenRFPerm returns a
// file refnum and makes the newly opened file current.
#[test]
fn open_rf_perm_present_file_returns_refnum_and_sets_current_file() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200100u32;
disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
bus.write_word(sp, 1); // fsRdPerm
bus.write_word(sp + 2, 0); // vRefNum
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF); // result placeholder
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let refnum = bus.read_word(sp + 8);
assert_ne!(refnum as i16, -1);
assert_eq!(cpu.read_reg(Register::D0), refnum as u32);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(disp.current_resource_refnum(), refnum);
assert_eq!(disp.resource_file_name(refnum), Some("Shapes"));
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// MTb 1993 p. 1-65: if already open, OpenRFPerm returns the same refnum
// and does not make that file current.
#[test]
fn open_rf_perm_reopen_returns_existing_refnum_without_switching_current_file() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200200u32;
disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);
// First open: Shapes.
bus.write_word(sp, 1);
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let shapes_ref = bus.read_word(sp + 8);
assert_ne!(shapes_ref as i16, -1);
assert_eq!(disp.current_resource_refnum(), shapes_ref);
// Second open: Sounds.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF);
bus.write_pstring(name_ptr, b"Sounds");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let sounds_ref = bus.read_word(sp + 8);
assert_ne!(sounds_ref as i16, -1);
assert_ne!(sounds_ref, shapes_ref);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
assert_eq!(cpu.read_reg(Register::D0), sounds_ref as u32);
// Re-open Shapes: refnum re-used, current file unchanged.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), shapes_ref);
assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// OpenRFPerm should mirror the current refnum in D0 for both the first
// open and the already-open reuse path.
#[test]
fn open_rf_perm_mirrors_refnum_in_d0_on_success_and_reopen() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200480u32;
disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);
bus.write_word(sp, 1);
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let shapes_ref = bus.read_word(sp + 8);
assert_ne!(shapes_ref as i16, -1);
assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
assert_eq!(disp.current_resource_refnum(), shapes_ref);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF);
bus.write_pstring(name_ptr, b"Sounds");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let sounds_ref = bus.read_word(sp + 8);
assert_ne!(sounds_ref as i16, -1);
assert_eq!(cpu.read_reg(Register::D0), sounds_ref as u32);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), shapes_ref);
assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// MTb 1993 p. 1-65: if already open, OpenRFPerm returns the same refnum
// and does not make that file current, even when a different file is
// current at the time of the reopen.
#[test]
fn open_rf_perm_reopen_keeps_current_file_when_a_different_file_is_current() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200280u32;
disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);
// Open Shapes.
bus.write_word(sp, 1); // fsRdPerm
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let shapes_ref = bus.read_word(sp + 8);
assert_ne!(shapes_ref as i16, -1);
assert_eq!(disp.current_resource_refnum(), shapes_ref);
// Open Sounds so we can later make Shapes current again.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF);
bus.write_pstring(name_ptr, b"Sounds");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let sounds_ref = bus.read_word(sp + 8);
assert_ne!(sounds_ref as i16, -1);
assert_ne!(sounds_ref, shapes_ref);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
// Make Shapes current again, then reopen Sounds. The reopen must
// return the original refnum and leave Shapes current.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, shapes_ref);
let result = disp.dispatch_toolbox(true, 0x198, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(disp.current_resource_refnum(), shapes_ref);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_word(sp + 2, 0);
bus.write_long(sp + 4, name_ptr);
bus.write_word(sp + 8, 0xBEEF);
bus.write_pstring(name_ptr, b"Sounds");
let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), sounds_ref);
assert_eq!(disp.current_resource_refnum(), shapes_ref);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// Inside Macintosh Volume I (1985), p. I-115: OpenResFile opens the
// named resource file, returns a refnum, and makes it current.
#[test]
fn open_res_file_present_returns_refnum_and_sets_current_file() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200250u32;
disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let refnum = bus.read_word(sp + 4);
assert_ne!(refnum as i16, -1);
assert_eq!(cpu.read_reg(Register::D0), refnum as u32);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(disp.current_resource_refnum(), refnum);
assert_eq!(disp.resource_file_name(refnum), Some("Shapes"));
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// Inside Macintosh Volume I (1985), p. I-115: reopening an already-open
// resource file returns its refnum but does not make it current.
#[test]
fn open_res_file_reopen_returns_existing_refnum_without_switching_current_file() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200260u32;
disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let shapes_ref = bus.read_word(sp + 4);
assert_ne!(shapes_ref as i16, -1);
assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
assert_eq!(disp.current_resource_refnum(), shapes_ref);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 0xBEEF);
bus.write_pstring(name_ptr, b"Sounds");
let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let sounds_ref = bus.read_word(sp + 4);
assert_ne!(sounds_ref as i16, -1);
assert_ne!(sounds_ref, shapes_ref);
assert_eq!(cpu.read_reg(Register::D0), sounds_ref as u32);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), shapes_ref);
assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn open_res_file_canonical_reopen_reuses_existing_refnum_without_switching_current_file() {
// IM:I I-115: already-open OpenResFile returns the existing refnum
// and does not make that file current. This holds even when the
// second open spells the same file via a more explicit path that
// resolves to the same VFS resource fork.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200265u32;
disp.vfs_rsrc.insert("Folder/Shapes".to_string(), vec![]);
disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let shapes_ref = bus.read_word(sp + 4);
assert_ne!(shapes_ref as i16, -1);
assert_eq!(disp.current_resource_refnum(), shapes_ref);
assert_eq!(disp.resource_file_name(shapes_ref), Some("Folder/Shapes"));
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 0xBEEF);
bus.write_pstring(name_ptr, b"Sounds");
let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let sounds_ref = bus.read_word(sp + 4);
assert_ne!(sounds_ref as i16, -1);
assert_ne!(sounds_ref, shapes_ref);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 0xBEEF);
bus.write_pstring(name_ptr, b"Unix:Folder:Shapes");
let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), shapes_ref);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// Inside Macintosh Volume I (1985), p. I-115: on failure OpenResFile
// returns -1 and ResError reports the underlying file-system error.
#[test]
fn open_res_file_missing_returns_minus_one_and_fnferr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200270u32;
bus.write_long(sp, name_ptr);
bus.write_word(sp + 4, 0xBEEF);
bus.write_pstring(name_ptr, b"NoSuchOpenResFile");
let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), (-1i16) as u16);
assert_eq!(cpu.read_reg(Register::D0), (-1i32) as u32);
assert_eq!(bus.read_word(0x0A60), (-43i16) as u16);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// MTb 1993 pp. 1-62..1-64: HOpenResFile opens the requested resource
// fork, returns the refnum, and makes it current.
#[test]
fn hopenresfile_success_returns_refnum_and_sets_current_resource_file() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x2002f0u32;
disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
bus.write_word(sp, 1); // fsRdPerm
bus.write_long(sp + 2, name_ptr);
bus.write_long(sp + 6, 0); // dirID
bus.write_word(sp + 10, 0); // vRefNum
bus.write_word(sp + 12, 0xBEEF); // result
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let refnum = bus.read_word(sp + 12);
assert_ne!(refnum as i16, -1);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(disp.current_resource_refnum(), refnum);
assert_eq!(disp.resource_file_name(refnum), Some("Shapes"));
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
// MTb 1993 p. 1-63: HOpenResFile returns -1 and ResError reports the file
// error when it can't open the requested resource fork.
#[test]
fn hopenresfile_missing_file_returns_minus_one_and_fnferr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200300u32;
bus.write_word(sp, 1); // fsRdPerm
bus.write_long(sp + 2, name_ptr);
bus.write_long(sp + 6, 0); // dirID
bus.write_word(sp + 10, 0); // vRefNum
bus.write_word(sp + 12, 0xBEEF); // result
bus.write_pstring(name_ptr, b"NoSuchHOpenFile");
let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), (-1i16) as u16);
assert_eq!(bus.read_word(0x0A60), (-43i16) as u16);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
// MTb 1993 p. 1-63: already-open HOpenResFile returns the existing refnum
// and does not make that file current.
#[test]
fn hopenresfile_already_open_returns_same_refnum_without_switching_current_file() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200400u32;
disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);
// First open: Shapes.
bus.write_word(sp, 1);
bus.write_long(sp + 2, name_ptr);
bus.write_long(sp + 6, 0);
bus.write_word(sp + 10, 0);
bus.write_word(sp + 12, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let shapes_ref = bus.read_word(sp + 12);
assert_ne!(shapes_ref as i16, -1);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(disp.current_resource_refnum(), shapes_ref);
// Second open: Sounds.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_long(sp + 2, name_ptr);
bus.write_long(sp + 6, 0);
bus.write_word(sp + 10, 0);
bus.write_word(sp + 12, 0xBEEF);
bus.write_pstring(name_ptr, b"Sounds");
let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let sounds_ref = bus.read_word(sp + 12);
assert_ne!(sounds_ref as i16, -1);
assert_ne!(sounds_ref, shapes_ref);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
// Re-open Shapes: refnum re-used, current file unchanged.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_long(sp + 2, name_ptr);
bus.write_long(sp + 6, 0);
bus.write_word(sp + 10, 0);
bus.write_word(sp + 12, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), shapes_ref);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
#[test]
fn hopenresfile_canonical_reopen_reuses_existing_refnum_without_switching_current_file() {
// MTb 1993 p. 1-63: already-open HOpenResFile returns the existing
// refnum and does not make that file current, even when the reopen
// uses a canonicalized path spelling for the same fork.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x200410u32;
disp.vfs_rsrc.insert("Folder/Shapes".to_string(), vec![]);
disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);
bus.write_word(sp, 1);
bus.write_long(sp + 2, name_ptr);
bus.write_long(sp + 6, 0);
bus.write_word(sp + 10, 0);
bus.write_word(sp + 12, 0xBEEF);
bus.write_pstring(name_ptr, b"Shapes");
let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let shapes_ref = bus.read_word(sp + 12);
assert_ne!(shapes_ref as i16, -1);
assert_eq!(disp.current_resource_refnum(), shapes_ref);
assert_eq!(disp.resource_file_name(shapes_ref), Some("Folder/Shapes"));
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_long(sp + 2, name_ptr);
bus.write_long(sp + 6, 0);
bus.write_word(sp + 10, 0);
bus.write_word(sp + 12, 0xBEEF);
bus.write_pstring(name_ptr, b"Sounds");
let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let sounds_ref = bus.read_word(sp + 12);
assert_ne!(sounds_ref as i16, -1);
assert_ne!(sounds_ref, shapes_ref);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 1);
bus.write_long(sp + 2, name_ptr);
bus.write_long(sp + 6, 0);
bus.write_word(sp + 10, 0);
bus.write_word(sp + 12, 0xBEEF);
bus.write_pstring(name_ptr, b"Unix:Folder:Shapes");
let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), shapes_ref);
assert_eq!(disp.current_resource_refnum(), sounds_ref);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
// CloseResFile ($A99A)
#[test]
fn test_close_res_file() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 2); // refNum
let result = disp.dispatch_toolbox(true, 0x19A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
}
#[test]
fn test_close_res_file_invalid_refnum_sets_basiliskii_resfnotfound() {
let (mut disp, mut cpu, mut bus) = setup();
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([
(0, crate::trap::dispatch::ResourceFileMap::default()),
(2, crate::trap::dispatch::ResourceFileMap::default()),
]),
names: std::collections::HashMap::new(),
search_order: vec![0, 2],
current_file: 2,
});
bus.write_word(0x0A5A, 2);
bus.write_word(0x0A60, 0);
let sp = TEST_SP;
bus.write_word(sp, (-2i16) as u16);
let result = disp.dispatch_toolbox(true, 0x19A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(disp.current_resource_refnum(), 2);
assert_eq!(bus.read_word(0x0A5A), 2);
assert_eq!(bus.read_word(0x0A60), (-193i16) as u16);
}
// UseResFile ($A998)
#[test]
fn test_use_res_file() {
let (mut disp, mut cpu, mut bus) = setup();
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([
(0, crate::trap::dispatch::ResourceFileMap::default()),
(2, crate::trap::dispatch::ResourceFileMap::default()),
]),
names: std::collections::HashMap::new(),
search_order: vec![0, 2],
current_file: 0,
});
let sp = TEST_SP;
bus.write_word(sp, 2); // refNum
let result = disp.dispatch_toolbox(true, 0x198, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(disp.current_resource_refnum(), 2);
assert_eq!(bus.read_word(0x0A5A), 2);
}
#[test]
fn test_use_res_file_invalid_refnum_preserves_current_and_sets_resfnotfound() {
let (mut disp, mut cpu, mut bus) = setup();
disp.resources = Some(crate::trap::dispatch::LoadedResources {
files: std::collections::HashMap::from([
(0, crate::trap::dispatch::ResourceFileMap::default()),
(2, crate::trap::dispatch::ResourceFileMap::default()),
]),
names: std::collections::HashMap::new(),
search_order: vec![0, 2],
current_file: 2,
});
bus.write_word(0x0A5A, 2);
bus.write_word(0x0A60, 0);
let sp = TEST_SP;
bus.write_word(sp, (-2i16) as u16);
let result = disp.dispatch_toolbox(true, 0x198, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(disp.current_resource_refnum(), 2);
assert_eq!(bus.read_word(0x0A5A), 2);
assert_eq!(bus.read_word(0x0A60), (-193i16) as u16);
}
#[test]
fn uniqueid_scans_all_open_files_while_unique1id_uses_current_file_only() {
// Inside Macintosh Volume I (1985), p. I-121:
// UniqueID searches all open resource files for the type.
// Inside Macintosh Volume IV (1986), p. IV-16:
// Unique1ID applies the same rule to the current file only.
let (mut disp, mut cpu, mut bus) = setup();
disp.install_test_resource_in_file(&mut bus, 0, *b"STR ", 128, &[0x11]);
disp.install_test_resource_in_file(&mut bus, 2, *b"STR ", 129, &[0x22]);
if let Some(resources) = disp.resources.as_mut() {
resources.current_file = 0;
}
let sp = TEST_SP;
bus.write_long(sp, u32::from_be_bytes(*b"STR "));
bus.write_word(sp + 4, 0xBEEF);
let uniqueid = disp.dispatch_toolbox(true, 0x1C1, &mut cpu, &mut bus);
assert!(uniqueid.is_some());
assert!(uniqueid.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 130);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, u32::from_be_bytes(*b"STR "));
bus.write_word(sp + 4, 0xBEEF);
let unique1id = disp.dispatch_toolbox(true, 0x010, &mut cpu, &mut bus);
assert!(unique1id.is_some());
assert!(unique1id.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 129);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn uniqueid_family_returns_128_when_requested_type_is_absent() {
// Inside Macintosh Volume I (1985), p. I-121 and Volume IV (1986),
// p. IV-16: both routines return an unused ID for the requested type.
// In Systemless's HLE, candidate scans begin at 128.
let (mut disp, mut cpu, mut bus) = setup();
disp.install_test_resource_in_file(&mut bus, 0, *b"MENU", 200, &[0x33]);
disp.install_test_resource_in_file(&mut bus, 3, *b"MENU", 201, &[0x44]);
if let Some(resources) = disp.resources.as_mut() {
resources.current_file = 0;
}
let sp = TEST_SP;
bus.write_long(sp, u32::from_be_bytes(*b"STR "));
bus.write_word(sp + 4, 0xBEEF);
let uniqueid = disp.dispatch_toolbox(true, 0x1C1, &mut cpu, &mut bus);
assert!(uniqueid.is_some());
assert!(uniqueid.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 128);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, u32::from_be_bytes(*b"STR "));
bus.write_word(sp + 4, 0xBEEF);
let unique1id = disp.dispatch_toolbox(true, 0x010, &mut cpu, &mut bus);
assert!(unique1id.is_some());
assert!(unique1id.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 128);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// Inside Macintosh Volume I (1985), p. I-124: RmvResource removes the
// current-file map reference but does not dispose handle memory.
#[test]
fn rmveresource_removes_current_file_reference_without_disposing_handle_data() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = bus.alloc(6);
bus.write_bytes(data_ptr, &[0x10, 0x20, 0x30, 0x40, 0x50, 0x60]);
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
disp.resources = Some(LoadedResources {
files: HashMap::from([(
0,
ResourceFileMap {
loaded: HashMap::from([((*b"TEST", 7), data_ptr)]),
named: HashMap::from([((*b"TEST", "Sample".to_string()), (7, data_ptr))]),
attrs: HashMap::from([((*b"TEST", 7), 0u8)]),
map_attrs: 0,
},
)]),
names: HashMap::new(),
search_order: vec![0],
current_file: 0,
});
disp.loaded_handles.insert(handle, (data_ptr, *b"TEST", 7));
disp.resource_handle_files.insert(handle, 0);
let sp = TEST_SP;
bus.write_long(sp, handle);
let result = disp.dispatch_toolbox(true, 0x1AD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_long(handle), data_ptr);
assert!(!disp.loaded_handles.contains_key(&handle));
assert!(!disp.resource_handle_files.contains_key(&handle));
let file = &disp.resources.as_ref().unwrap().files[&0];
assert!(!file.loaded.contains_key(&(*b"TEST", 7)));
assert!(!file.attrs.contains_key(&(*b"TEST", 7)));
assert!(file.named.is_empty());
}
// Inside Macintosh Volume I (1985), p. I-124: resProtected resources are
// not removed and RmvResource returns rmvResFailed.
#[test]
fn rmveresource_protected_handle_returns_rmvresfailed() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = bus.alloc(4);
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
disp.resources = Some(LoadedResources {
files: HashMap::from([(
0,
ResourceFileMap {
loaded: HashMap::from([((*b"PROT", 9), data_ptr)]),
named: HashMap::new(),
attrs: HashMap::from([((*b"PROT", 9), 0x0008u8)]),
map_attrs: 0,
},
)]),
names: HashMap::new(),
search_order: vec![0],
current_file: 0,
});
disp.loaded_handles.insert(handle, (data_ptr, *b"PROT", 9));
disp.resource_handle_files.insert(handle, 0);
let sp = TEST_SP;
bus.write_long(sp, handle);
let result = disp.dispatch_toolbox(true, 0x1AD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(0x0A60) as i16, -196);
assert!(disp.loaded_handles.contains_key(&handle));
assert!(disp.resources.as_ref().unwrap().files[&0]
.loaded
.contains_key(&(*b"PROT", 9)));
}
// Inside Macintosh Volume I (1985), p. I-124: noncurrent-file resources
// fail with rmvResFailed.
#[test]
fn rmveresource_noncurrent_file_handle_returns_rmvresfailed() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = bus.alloc(4);
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
disp.resources = Some(LoadedResources {
files: HashMap::from([
(0, ResourceFileMap::default()),
(
2,
ResourceFileMap {
loaded: HashMap::from([((*b"OTHR", 3), data_ptr)]),
named: HashMap::new(),
attrs: HashMap::from([((*b"OTHR", 3), 0u8)]),
map_attrs: 0,
},
),
]),
names: HashMap::new(),
search_order: vec![0, 2],
current_file: 0,
});
disp.loaded_handles.insert(handle, (data_ptr, *b"OTHR", 3));
disp.resource_handle_files.insert(handle, 2);
let sp = TEST_SP;
bus.write_long(sp, handle);
let result = disp.dispatch_toolbox(true, 0x1AD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(0x0A60) as i16, -196);
assert!(disp.loaded_handles.contains_key(&handle));
assert!(disp.resources.as_ref().unwrap().files[&2]
.loaded
.contains_key(&(*b"OTHR", 3)));
}
// Inside Macintosh Volume I (1985), p. I-124: non-resource handles return
// rmvResFailed.
#[test]
fn rmveresource_non_resource_handle_returns_rmvresfailed() {
let (mut disp, mut cpu, mut bus) = setup();
let fake_ptr = bus.alloc(4);
let fake_handle = bus.alloc(4);
bus.write_long(fake_handle, fake_ptr);
let sp = TEST_SP;
bus.write_long(sp, fake_handle);
let result = disp.dispatch_toolbox(true, 0x1AD, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(0x0A60) as i16, -196);
assert_eq!(bus.read_long(fake_handle), fake_ptr);
}
// Inside Macintosh Volume I (1985), p. I-124: RmveReference is an
// obsolete alias for RmveResource / RemoveResource and leaves the
// handle data allocated.
#[test]
fn rmverereference_removes_current_file_reference_without_disposing_handle_data() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = bus.alloc(6);
bus.write_bytes(data_ptr, &[0x10, 0x20, 0x30, 0x40, 0x50, 0x60]);
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
disp.resources = Some(LoadedResources {
files: HashMap::from([(
0,
ResourceFileMap {
loaded: HashMap::from([((*b"TEST", 7), data_ptr)]),
named: HashMap::from([((*b"TEST", "Sample".to_string()), (7, data_ptr))]),
attrs: HashMap::from([((*b"TEST", 7), 0u8)]),
map_attrs: 0,
},
)]),
names: HashMap::new(),
search_order: vec![0],
current_file: 0,
});
disp.loaded_handles.insert(handle, (data_ptr, *b"TEST", 7));
disp.resource_handle_files.insert(handle, 0);
let sp = TEST_SP;
bus.write_long(sp, handle);
let result = disp.dispatch_toolbox(true, 0x1AE, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(bus.read_long(handle), data_ptr);
assert!(!disp.loaded_handles.contains_key(&handle));
assert!(!disp.resource_handle_files.contains_key(&handle));
let file = &disp.resources.as_ref().unwrap().files[&0];
assert!(!file.loaded.contains_key(&(*b"TEST", 7)));
assert!(!file.attrs.contains_key(&(*b"TEST", 7)));
assert!(file.named.is_empty());
}
// Inside Macintosh Volume I (1985), p. I-125: UpdateResFile flushes
// changed state for the requested open resource file.
#[test]
fn updateresfile_clears_reschanged_bits_for_target_open_file() {
let (mut disp, mut cpu, mut bus) = setup();
let changed = super::super::TrapDispatcher::RES_CHANGED_ATTR as u8;
disp.resources = Some(LoadedResources {
files: HashMap::from([
(
0,
ResourceFileMap {
loaded: HashMap::from([((*b"CURR", 1), 0x1000)]),
named: HashMap::new(),
attrs: HashMap::from([((*b"CURR", 1), changed)]),
map_attrs: 0,
},
),
(
2,
ResourceFileMap {
loaded: HashMap::from([((*b"TARG", 2), 0x2000)]),
named: HashMap::new(),
attrs: HashMap::from([((*b"TARG", 2), changed | 0x0008u8)]),
map_attrs: 0,
},
),
]),
names: HashMap::new(),
search_order: vec![0, 2],
current_file: 0,
});
let sp = TEST_SP;
bus.write_word(sp, 2);
let result = disp.dispatch_toolbox(true, 0x199, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(bus.read_word(0x0A60), 0);
let resources = disp.resources.as_ref().unwrap();
let target_attrs = resources.files[&2].attrs[&(*b"TARG", 2)];
let current_attrs = resources.files[&0].attrs[&(*b"CURR", 1)];
assert_eq!(target_attrs & changed, 0);
assert_eq!(target_attrs & 0x0008u8, 0x0008u8);
assert_ne!(current_attrs & changed, 0);
}
// Inside Macintosh Volume I (1985), p. I-125: UpdateResFile on an unknown
// refNum reports resFNotFound.
#[test]
fn updateresfile_invalid_refnum_returns_resfnotfound() {
let (mut disp, mut cpu, mut bus) = setup();
disp.resources = Some(LoadedResources {
files: HashMap::from([(0, ResourceFileMap::default())]),
names: HashMap::new(),
search_order: vec![0],
current_file: 0,
});
let sp = TEST_SP;
bus.write_word(sp, 99);
bus.write_word(0x0A60, 0);
let result = disp.dispatch_toolbox(true, 0x199, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(bus.read_word(0x0A60) as i16, -193);
}
// Inside Macintosh Volume I (1985), p. I-126: SetResPurge takes one
// BOOLEAN argument and therefore consumes one word from the stack.
#[test]
fn setrespurge_reads_high_byte_boolean_and_consumes_argument() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x00FF);
bus.write_word(sp + 2, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x193, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert!(!disp.res_purge);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(bus.read_word(sp + 2), 0xBEEF);
}
// Inside Macintosh Volume I (1985), p. I-126: SetResPurge installs
// or removes the resource purge handler based on install.
#[test]
fn setrespurge_toggles_resource_purge_install_flag() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0100);
let result = disp.dispatch_toolbox(true, 0x193, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert!(disp.res_purge);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x00FF);
let result = disp.dispatch_toolbox(true, 0x193, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert!(!disp.res_purge);
}
// IM:I I-302 + IM:I I-91: DragGrayRgn is the gray-outline alias of
// DragTheRgn, and the non-drag path returns the $80008000 sentinel.
#[test]
fn draggrayrgn_returns_no_drag_sentinel_and_consumes_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP - 22;
cpu.write_reg(Register::A7, sp);
for i in 0..22u32 {
bus.write_byte(sp + i, 0xAA);
}
bus.write_long(sp + 22, 0xDEAD_BEEF);
let result = disp.dispatch_toolbox(true, 0x105, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
assert_eq!(bus.read_long(TEST_SP), 0x8000_8000);
}
// SetResLoad ($A99B) — Mac Pascal Boolean is in the high byte
#[test]
fn test_set_res_load_reads_high_byte_boolean() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// IM:More Macintosh Toolbox 1993, 1-79 plus MPW stack convention:
// Boolean FALSE is $00 in the high byte. The low byte is padding and
// must not turn the parameter true.
disp.res_load = true;
bus.write_word(sp, 0x00FF);
bus.write_word(0x0A60, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x19B, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert!(!disp.res_load);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
// TRUE is $01 in the high byte.
cpu.write_reg(Register::A7, sp);
bus.write_word(0x0A60, 0xBEEF);
bus.write_word(sp, 0x0100);
let result = disp.dispatch_toolbox(true, 0x19B, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert!(disp.res_load);
assert_eq!(bus.read_word(0x0A60), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
}
// CountResources ($A99C)
#[test]
fn test_count_resources() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// SP+0: type(4)
bus.write_long(sp, 0x49434E23); // 'ICN#'
bus.write_word(sp + 4, 0xBEEF); // result placeholder
let result = disp.dispatch_toolbox(true, 0x19C, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// Count1Resources ($A80D)
#[test]
fn test_count1_resources() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x49434E23); // 'ICN#'
bus.write_word(sp + 4, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x00D, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// GetNamedResource ($A9A1)
#[test]
fn test_get_named_resource_searches_resource_chain() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = bus.alloc(16);
bus.write_bytes(data_ptr, &[0x42; 16]);
let mut loaded = HashMap::new();
loaded.insert((*b"STR ", 500i16), data_ptr);
let mut named = HashMap::new();
named.insert((*b"STR ", "MyString".to_string()), (500i16, data_ptr));
disp.resources = Some(LoadedResources {
files: HashMap::from([
(0, ResourceFileMap::default()),
(
2,
ResourceFileMap {
loaded,
named,
attrs: HashMap::new(),
map_attrs: 0,
},
),
]),
names: HashMap::new(),
search_order: vec![0, 2],
current_file: 2,
});
let name_addr = 0x200000u32;
bus.write_byte(name_addr, 8);
bus.write_bytes(name_addr + 1, b"MyString");
let sp = TEST_SP;
bus.write_long(sp, name_addr);
bus.write_long(sp + 4, u32::from_be_bytes(*b"STR "));
let result = disp.dispatch_toolbox(true, 0x1A1, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(0x0A60), 0);
assert_ne!(bus.read_long(sp + 8), 0);
}
// GetResAttrs ($A9A6) — handler now lives in resource.rs (see
// dispatcher chain in dispatch.rs: dispatch_resource runs before
// dispatch_toolbox). Test against dispatch_resource directly so the
// assertion exercises the canonical path.
#[test]
fn test_get_res_attrs() {
let (mut disp, mut cpu, mut bus) = setup();
let data_ptr = bus.alloc(8);
bus.write_bytes(data_ptr, &[0x11; 8]);
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
disp.loaded_handles.insert(handle, (data_ptr, *b"TEST", 7));
disp.resource_handle_files.insert(handle, 0);
disp.resources = Some(LoadedResources {
files: HashMap::from([(
0,
ResourceFileMap {
loaded: HashMap::from([((*b"TEST", 7), data_ptr)]),
named: HashMap::new(),
attrs: HashMap::from([((*b"TEST", 7), 0x002Cu8)]),
map_attrs: 0,
},
)]),
names: HashMap::new(),
search_order: vec![0],
current_file: 0,
});
let sp = TEST_SP;
// SP+0: handle(4)
bus.write_long(sp, handle);
bus.write_word(sp + 4, 0xBEEF);
let result = disp.dispatch_resource(true, 0x1A6, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0x002C);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(bus.read_word(0x0A60), 0);
}
// ========== Misc Toolbox ==========
// Inside Macintosh Volume I (1985), p. I-287: DrawGrowIcon draws
// delimiter lines 15 pixels in from the right/bottom edges of portRect.
#[test]
fn drawgrowicon_draws_scrollbar_delimiter_lines_15_pixels_in_from_portrect_edges() {
let (mut disp, mut cpu, mut bus) = setup();
let screen_base = bus.alloc(64 * 342);
disp.screen_mode = (screen_base, 64, 512, 342, 1);
let window_ptr = bus.alloc(256);
bus.write_word(window_ptr + 6, 0x0000); // GrafPort path, not CGrafPort
bus.write_word(window_ptr + 8, 0); // portBits.bounds.top
bus.write_word(window_ptr + 10, 0); // portBits.bounds.left
bus.write_word(window_ptr + 16, 30); // portRect.top
bus.write_word(window_ptr + 18, 20); // portRect.left
bus.write_word(window_ptr + 20, 70); // portRect.bottom
bus.write_word(window_ptr + 22, 100); // portRect.right
let sp = TEST_SP - 4;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, window_ptr);
let result = disp.dispatch_toolbox(true, 0x104, &mut cpu, &mut bus);
assert!(result.is_some(), "DrawGrowIcon should be handled");
assert!(result.unwrap().is_ok(), "DrawGrowIcon should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP,
"DrawGrowIcon should pop one WindowPtr argument"
);
let sep_x = 100 - 15;
let sep_y = 70 - 15;
let row_bytes = 64;
assert!(
read_screen_pixel_1bpp(&bus, screen_base, row_bytes, sep_x, 30),
"vertical delimiter should start at content top"
);
assert!(
read_screen_pixel_1bpp(&bus, screen_base, row_bytes, sep_x, 69),
"vertical delimiter should reach content bottom-1"
);
assert!(
read_screen_pixel_1bpp(&bus, screen_base, row_bytes, 19, sep_y),
"horizontal delimiter should start at content left-1"
);
assert!(
read_screen_pixel_1bpp(&bus, screen_base, row_bytes, 101, sep_y),
"horizontal delimiter should reach content right+1"
);
}
// Inside Macintosh Volume I (1985), p. I-287: DrawGrowIcon is a
// PROCEDURE DrawGrowIcon(theWindow: WindowPtr).
#[test]
fn drawgrowicon_consumes_windowptr_argument() {
let (mut disp, mut cpu, mut bus) = setup();
let window_ptr = 0x234000u32;
let sp = TEST_SP - 4;
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, window_ptr);
let result = disp.dispatch_toolbox(true, 0x104, &mut cpu, &mut bus);
assert!(result.is_some(), "DrawGrowIcon should be handled");
assert!(result.unwrap().is_ok(), "DrawGrowIcon should succeed");
assert_eq!(
cpu.read_reg(Register::A7),
TEST_SP,
"DrawGrowIcon should pop one WindowPtr argument"
);
}
// Munger ($A9E0)
// IM:I 1985 p. I-468: replace first found target and return offset of
// first byte past replacement.
#[test]
fn munger_replaces_first_occurrence_and_returns_offset_past_replacement() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let ptr1 = 0x310000u32;
let ptr2 = 0x310100u32;
bus.write_bytes(ptr1, b"^0");
bus.write_bytes(ptr2, b"Ace");
let data_ptr = bus.alloc(9);
bus.write_bytes(data_ptr, b"Hello, ^0");
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
bus.write_long(sp, 3); // len2
bus.write_long(sp + 4, ptr2);
bus.write_long(sp + 8, 2); // len1
bus.write_long(sp + 12, ptr1);
bus.write_long(sp + 16, 0); // offset
bus.write_long(sp + 20, handle);
bus.write_long(sp + 24, 0);
let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 24), 10);
assert_eq!(cpu.read_reg(Register::A7), sp + 24);
let final_ptr = bus.read_long(handle);
let final_size = bus.get_alloc_size(final_ptr).unwrap();
assert_eq!(
bus.read_bytes(final_ptr, final_size as usize),
b"Hello, Ace"
);
}
// XMunger ($A819) is a phantom trap word that BasiliskII exposes
// as a no-op/no-pop stub.
#[test]
fn xmunger_phantom_noop_leaves_handle_unchanged_and_stack_unbalanced() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let ptr1 = 0x312000u32;
let ptr2 = 0x312100u32;
bus.write_bytes(ptr1, b"^0");
bus.write_bytes(ptr2, b"Ace");
let data_ptr = bus.alloc(9);
bus.write_bytes(data_ptr, b"Hello, ^0");
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
bus.write_long(sp, 3); // len2
bus.write_long(sp + 4, ptr2);
bus.write_long(sp + 8, 2); // len1
bus.write_long(sp + 12, ptr1);
bus.write_long(sp + 16, 0); // offset
bus.write_long(sp + 20, handle);
bus.write_long(sp + 24, 0);
let result = disp.dispatch_toolbox(true, 0x019, &mut cpu, &mut bus);
assert!(result.is_some(), "XMunger should be handled");
assert!(result.unwrap().is_ok(), "XMunger should succeed");
assert_eq!(bus.read_long(sp + 24), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
assert_eq!(
bus.read_long(handle),
data_ptr,
"XMunger should leave the handle pointer unchanged"
);
let final_ptr = bus.read_long(handle);
let final_size = bus.get_alloc_size(final_ptr).unwrap();
assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"Hello, ^0");
}
// IM:I 1985 p. I-469: if ptr2 is NIL, return match offset and leave
// destination bytes unchanged.
#[test]
fn munger_search_only_mode_returns_match_offset_without_modifying_destination() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let ptr1 = 0x310000u32;
bus.write_bytes(ptr1, b"the");
let initial = b"there's the apple";
let data_ptr = bus.alloc(initial.len() as u32);
bus.write_bytes(data_ptr, initial);
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
bus.write_long(sp, 0); // len2
bus.write_long(sp + 4, 0); // ptr2 = NIL
bus.write_long(sp + 8, 3); // len1
bus.write_long(sp + 12, ptr1);
bus.write_long(sp + 16, 4); // offset
bus.write_long(sp + 20, handle);
bus.write_long(sp + 24, 0);
let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 24), 8);
assert_eq!(cpu.read_reg(Register::A7), sp + 24);
let final_ptr = bus.read_long(handle);
let final_size = bus.get_alloc_size(final_ptr).unwrap();
assert_eq!(bus.read_bytes(final_ptr, final_size as usize), initial);
}
// IM:I 1985 p. I-469: len1 == 0 inserts replacement bytes at offset and
// returns first byte past insertion.
#[test]
fn munger_len1_zero_inserts_replacement_at_offset() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let ptr2 = 0x310100u32;
bus.write_bytes(ptr2, b"X");
let data_ptr = bus.alloc(5);
bus.write_bytes(data_ptr, b"apple");
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
bus.write_long(sp, 1); // len2
bus.write_long(sp + 4, ptr2);
bus.write_long(sp + 8, 0); // len1
bus.write_long(sp + 12, 0); // ptr1 ignored
bus.write_long(sp + 16, 2); // offset
bus.write_long(sp + 20, handle);
bus.write_long(sp + 24, 0);
let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 24), 3);
assert_eq!(cpu.read_reg(Register::A7), sp + 24);
let final_ptr = bus.read_long(handle);
let final_size = bus.get_alloc_size(final_ptr).unwrap();
assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"apXple");
}
// IM:I 1985 p. I-469: len2 == 0 with non-NIL ptr2 deletes the target
// substring and returns the deletion offset.
#[test]
fn munger_len2_zero_deletes_target_and_returns_deletion_offset() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let ptr1 = 0x310000u32;
let ptr2 = 0x310100u32;
bus.write_bytes(ptr1, b"123");
bus.write_bytes(ptr2, b"z");
let data_ptr = bus.alloc(9);
bus.write_bytes(data_ptr, b"abc123def");
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
bus.write_long(sp, 0); // len2
bus.write_long(sp + 4, ptr2); // ptr2 non-NIL keeps delete path
bus.write_long(sp + 8, 3); // len1
bus.write_long(sp + 12, ptr1);
bus.write_long(sp + 16, 0); // offset
bus.write_long(sp + 20, handle);
bus.write_long(sp + 24, 0);
let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 24), 3);
assert_eq!(cpu.read_reg(Register::A7), sp + 24);
let final_ptr = bus.read_long(handle);
let final_size = bus.get_alloc_size(final_ptr).unwrap();
assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"abcdef");
}
// IM:I 1985 p. I-469: returns a negative value when target is not found.
#[test]
fn munger_returns_negative_when_target_not_found() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let ptr1 = 0x310000u32;
let ptr2 = 0x310100u32;
bus.write_bytes(ptr1, b"zz");
bus.write_bytes(ptr2, b"A");
let data_ptr = bus.alloc(5);
bus.write_bytes(data_ptr, b"hello");
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
bus.write_long(sp, 1); // len2
bus.write_long(sp + 4, ptr2);
bus.write_long(sp + 8, 2); // len1
bus.write_long(sp + 12, ptr1);
bus.write_long(sp + 16, 0); // offset
bus.write_long(sp + 20, handle);
bus.write_long(sp + 24, 0);
let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 24) as i32, -1);
assert_eq!(cpu.read_reg(Register::A7), sp + 24);
let final_ptr = bus.read_long(handle);
let final_size = bus.get_alloc_size(final_ptr).unwrap();
assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"hello");
}
// BasiliskII/System 7.5 ROM: if the tail at offset only partially
// matches the beginning of the target, return -1 and leave the
// destination unchanged.
#[test]
fn munger_partial_tail_match_at_offset_returns_negative_and_leaves_tail_unchanged() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let ptr1 = 0x310000u32;
let ptr2 = 0x310100u32;
bus.write_bytes(ptr1, b"abc");
bus.write_bytes(ptr2, b"Z");
let data_ptr = bus.alloc(2);
bus.write_bytes(data_ptr, b"ab");
let handle = bus.alloc(4);
bus.write_long(handle, data_ptr);
bus.write_long(sp, 1); // len2
bus.write_long(sp + 4, ptr2);
bus.write_long(sp + 8, 3); // len1
bus.write_long(sp + 12, ptr1);
bus.write_long(sp + 16, 0); // offset
bus.write_long(sp + 20, handle);
bus.write_long(sp + 24, 0);
let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 24) as i32, -1);
assert_eq!(cpu.read_reg(Register::A7), sp + 24);
let final_ptr = bus.read_long(handle);
let final_size = bus.get_alloc_size(final_ptr).unwrap();
assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"ab");
}
// PBOpenRF ($A00A) — OS trap, file not found
#[test]
fn test_pb_open_rf_not_found() {
let (mut disp, mut cpu, mut bus) = setup();
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
// Set up Pascal string filename at 0x310000
let name_ptr = 0x310000u32;
bus.write_pstring(name_ptr, b"MissingFile.rsrc");
// Write name_ptr into param block at pb+18
bus.write_long(pb + 18, name_ptr);
// Clear ioResult at pb+16
bus.write_word(pb + 16, 0);
let result = disp.dispatch_toolbox(false, 0x0A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// ioResult at pb+16 = -43 (fnfErr)
assert_eq!(bus.read_word(pb + 16), (-43i16) as u16);
assert_eq!(cpu.read_reg(Register::D0), (-43i32) as u32);
}
// HCreateResFile ($A81B) — missing file creation plus PBOpenRF visibility.
#[test]
fn test_hcreate_res_file_creates_missing_file_and_resource_fork() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let name_ptr = 0x310000u32;
bus.write_pstring(name_ptr, b"Prefs.RSRC");
bus.write_long(sp, name_ptr);
bus.write_long(sp + 4, 0); // dirID
bus.write_word(sp + 8, 0); // vRefNum
cpu.write_reg(Register::A7, sp);
let result = disp.dispatch_toolbox(true, 0x01B, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(bus.read_word(0x0A60), 0);
assert!(disp.vfs.contains_key("Prefs.RSRC"));
assert!(disp.vfs_rsrc.contains_key("Prefs.RSRC"));
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
bus.write_long(pb + 18, name_ptr);
bus.write_word(pb + 22, 0);
bus.write_byte(pb + 27, 3); // fsRdWrPerm
bus.write_long(pb + 48, 0);
let result = disp.dispatch_toolbox(false, 0x0A, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(pb + 16), 0);
assert!(bus.read_word(pb + 24) > 0);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
#[test]
fn test_hcreate_res_file_existing_resource_fork_returns_dupfnerr_and_preserves_file() {
let (mut disp, mut cpu, mut bus) = setup();
disp.vfs.insert("Prefs.RSRC".to_string(), vec![0x11, 0x22]);
disp.vfs_rsrc
.insert("Prefs.RSRC".to_string(), vec![0x33, 0x44]);
let sp = TEST_SP;
let name_ptr = 0x310100u32;
bus.write_pstring(name_ptr, b"Prefs.RSRC");
bus.write_long(sp, name_ptr);
bus.write_long(sp + 4, 0);
bus.write_word(sp + 8, 0);
cpu.write_reg(Register::A7, sp);
let result = disp.dispatch_toolbox(true, 0x01B, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(bus.read_word(0x0A60) as i16, -48);
assert_eq!(disp.vfs.get("Prefs.RSRC").unwrap(), &vec![0x11, 0x22]);
assert_eq!(disp.vfs_rsrc.get("Prefs.RSRC").unwrap(), &vec![0x33, 0x44]);
}
// PBOpenRF on a freshly-PBCreate'd rsrc fork that's already had
// FSWrite bytes appended must NOT clobber the in-memory rsrc bytes
// (vfs[__rsrc__name]) with the snapshot from vfs_rsrc.
#[test]
fn test_pb_open_rf_preserves_in_progress_writes() {
let (mut disp, mut cpu, mut bus) = setup();
let name = "InstallerTemp";
let rsrc_key = "__rsrc__InstallerTemp";
// Simulate a previous open that wrote 4 bytes through the
// open-files path. vfs has the new bytes; vfs_rsrc still has
// the original empty snapshot.
disp.vfs_rsrc.insert(name.to_string(), Vec::new());
disp.vfs
.insert(rsrc_key.to_string(), vec![0xCA, 0xFE, 0xBA, 0xBE]);
let pb = 0x300000u32;
cpu.write_reg(Register::A0, pb);
let name_ptr = 0x310000u32;
bus.write_pstring(name_ptr, name.as_bytes());
bus.write_long(pb + 18, name_ptr);
disp.dispatch_toolbox(false, 0x0A, &mut cpu, &mut bus)
.unwrap()
.unwrap();
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(
disp.vfs.get(rsrc_key).unwrap(),
&vec![0xCA, 0xFE, 0xBA, 0xBE],
"Re-opening must not clobber the in-progress rsrc-fork \
writes with the on-disk snapshot"
);
}
// Pack0 / List Manager ($A9E7) — LNew selector $0044
// IM:IV 1986 p. IV-269: LNew returns ListHandle; selFlags=0 and active=TRUE.
#[test]
fn pack0_lnew_returns_non_nil_listhandle_with_default_selection_and_active_flags() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let view_rect_ptr = 0x350000u32;
let data_bounds_ptr = 0x350100u32;
let window_ptr = 0x210000u32;
// view = (0,0)-(40,80), dataBounds = rows 0..2, cols 0..1
bus.write_word(view_rect_ptr, 0);
bus.write_word(view_rect_ptr + 2, 0);
bus.write_word(view_rect_ptr + 4, 40);
bus.write_word(view_rect_ptr + 6, 80);
bus.write_word(data_bounds_ptr, 0);
bus.write_word(data_bounds_ptr + 2, 0);
bus.write_word(data_bounds_ptr + 4, 2);
bus.write_word(data_bounds_ptr + 6, 1);
bus.write_word(sp, 0x0044); // LNew selector
bus.write_word(sp + 2, 1); // drawIt = TRUE
bus.write_word(sp + 4, 0); // hasGrow = FALSE
bus.write_word(sp + 6, 0); // scrollHoriz = FALSE
bus.write_word(sp + 8, 0); // scrollVert = FALSE
bus.write_long(sp + 10, window_ptr);
bus.write_word(sp + 14, 0); // default LDEF
bus.write_word(sp + 16, 0); // cSize.v => default
bus.write_word(sp + 18, 0); // cSize.h => default
bus.write_long(sp + 20, data_bounds_ptr);
bus.write_long(sp + 24, view_rect_ptr);
bus.write_long(sp + 28, 0); // result slot
let result = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 28);
let list_handle = bus.read_long(sp + 28);
assert_ne!(list_handle, 0);
let list_ptr = bus.read_long(list_handle);
assert_ne!(list_ptr, 0);
assert_eq!(
bus.read_byte(list_ptr + 36),
0,
"selFlags should default to 0"
);
assert_eq!(
bus.read_byte(list_ptr + 37),
1,
"lActive should default to TRUE"
);
}
// Pack0 / List Manager ($A9E7) — LAddRow selector $0008
// IM:IV 1986 p. IV-271: returns first added row and increases dataBounds.bottom.
#[test]
fn pack0_laddrow_returns_insert_row_and_extends_databounds_bottom() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let view_rect_ptr = 0x351000u32;
let data_bounds_ptr = 0x351100u32;
bus.write_word(view_rect_ptr, 0);
bus.write_word(view_rect_ptr + 2, 0);
bus.write_word(view_rect_ptr + 4, 40);
bus.write_word(view_rect_ptr + 6, 80);
bus.write_word(data_bounds_ptr, 0);
bus.write_word(data_bounds_ptr + 2, 0);
bus.write_word(data_bounds_ptr + 4, 2);
bus.write_word(data_bounds_ptr + 6, 1);
bus.write_word(sp, 0x0044); // LNew
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, 0x210000);
bus.write_word(sp + 14, 0);
bus.write_word(sp + 16, 10);
bus.write_word(sp + 18, 40);
bus.write_long(sp + 20, data_bounds_ptr);
bus.write_long(sp + 24, view_rect_ptr);
bus.write_long(sp + 28, 0);
let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(create.is_some());
assert!(create.unwrap().is_ok());
let list_handle = bus.read_long(sp + 28);
cpu.write_reg(Register::A7, sp);
// Pascal calling convention: lHandle (last arg) is closest to the
// selector, count (first arg) is at the deepest slot.
bus.write_word(sp, 0x0008); // LAddRow
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 1); // rowNum
bus.write_word(sp + 8, 1); // count
bus.write_word(sp + 10, 0xBEEF); // INTEGER result slot
let add = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(add.is_some());
assert!(add.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 10), 1);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
let list_ptr = bus.read_long(list_handle);
let data_bounds_bottom = bus.read_word(list_ptr + 76) as i16;
assert_eq!(data_bounds_bottom, 3);
}
// Pack0 / List Manager ($A9E7) — LAddToCell selector $000C
// IM:IV 1986 p. IV-272: append bytes into an existing cell; invalid cells are ignored.
#[test]
fn pack0_laddtocell_appends_data_and_ignores_invalid_cells() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let view_rect_ptr = 0x352000u32;
let data_bounds_ptr = 0x352100u32;
let seed_ptr = 0x352200u32;
let append_ptr = 0x352210u32;
let valid_out_ptr = 0x352220u32;
let valid_len_ptr = 0x352230u32;
bus.write_word(view_rect_ptr, 0);
bus.write_word(view_rect_ptr + 2, 0);
bus.write_word(view_rect_ptr + 4, 48);
bus.write_word(view_rect_ptr + 6, 80);
bus.write_word(data_bounds_ptr, 0);
bus.write_word(data_bounds_ptr + 2, 0);
bus.write_word(data_bounds_ptr + 4, 1);
bus.write_word(data_bounds_ptr + 6, 1);
bus.write_word(sp, 0x0044); // LNew
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, 0x210000);
bus.write_word(sp + 14, 0);
bus.write_word(sp + 16, 12);
bus.write_word(sp + 18, 24);
bus.write_long(sp + 20, data_bounds_ptr);
bus.write_long(sp + 24, view_rect_ptr);
bus.write_long(sp + 28, 0);
let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(create.is_some());
assert!(create.unwrap().is_ok());
let list_handle = bus.read_long(sp + 28);
assert_ne!(list_handle, 0);
bus.write_bytes(seed_ptr, b"A");
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0058); // LSetCell
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_word(sp + 10, 1);
bus.write_long(sp + 12, seed_ptr);
let set_seed = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(set_seed.is_some());
assert!(set_seed.unwrap().is_ok());
bus.write_bytes(append_ptr, b"BC");
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x000C); // LAddToCell
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_word(sp + 10, 2);
bus.write_long(sp + 12, append_ptr);
let add = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(add.is_some());
assert!(add.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 16);
bus.write_word(valid_len_ptr, 8);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0038); // LGetCell
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, valid_len_ptr);
bus.write_long(sp + 14, valid_out_ptr);
let get_valid = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(get_valid.is_some());
assert!(get_valid.unwrap().is_ok());
assert_eq!(bus.read_word(valid_len_ptr), 3);
assert_eq!(bus.read_byte(valid_out_ptr), b'A');
assert_eq!(bus.read_byte(valid_out_ptr + 1), b'B');
assert_eq!(bus.read_byte(valid_out_ptr + 2), b'C');
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x000C); // LAddToCell
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 1); // invalid row
bus.write_word(sp + 8, 0);
bus.write_word(sp + 10, 2);
bus.write_long(sp + 12, append_ptr);
let add_invalid = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(add_invalid.is_some());
assert!(add_invalid.unwrap().is_ok());
bus.write_word(valid_len_ptr, 8);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0038); // LGetCell
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, valid_len_ptr);
bus.write_long(sp + 14, valid_out_ptr);
let get_invalid = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(get_invalid.is_some());
assert!(get_invalid.unwrap().is_ok());
assert_eq!(bus.read_word(valid_len_ptr), 3);
assert_eq!(bus.read_byte(valid_out_ptr), b'A');
assert_eq!(bus.read_byte(valid_out_ptr + 1), b'B');
assert_eq!(bus.read_byte(valid_out_ptr + 2), b'C');
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0028); // LDispose
bus.write_long(sp + 2, list_handle);
let dispose = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(dispose.is_some());
assert!(dispose.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
// Pack0 / List Manager ($A9E7) — LDelRow selector $0024
// IM:IV 1986 p. IV-271: rows after rowNum shift by count; dataBounds.bottom decreases.
#[test]
fn pack0_ldelrow_deletes_rows_compacts_following_rows_and_reduces_databounds_bottom() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let view_rect_ptr = 0x352000u32;
let data_bounds_ptr = 0x352100u32;
let data_a_ptr = 0x352200u32;
let data_b_ptr = 0x352210u32;
let out_ptr = 0x352220u32;
let out_len_ptr = 0x352230u32;
bus.write_word(view_rect_ptr, 0);
bus.write_word(view_rect_ptr + 2, 0);
bus.write_word(view_rect_ptr + 4, 40);
bus.write_word(view_rect_ptr + 6, 80);
bus.write_word(data_bounds_ptr, 0);
bus.write_word(data_bounds_ptr + 2, 0);
bus.write_word(data_bounds_ptr + 4, 2); // two rows: 0 and 1
bus.write_word(data_bounds_ptr + 6, 1); // one column
bus.write_word(sp, 0x0044); // LNew
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, 0x210000);
bus.write_word(sp + 14, 0);
bus.write_word(sp + 16, 10);
bus.write_word(sp + 18, 40);
bus.write_long(sp + 20, data_bounds_ptr);
bus.write_long(sp + 24, view_rect_ptr);
bus.write_long(sp + 28, 0);
let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(create.is_some());
assert!(create.unwrap().is_ok());
let list_handle = bus.read_long(sp + 28);
// LSetCell row 0 = "A"
bus.write_bytes(data_a_ptr, b"A");
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0058); // LSetCell
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0); // row
bus.write_word(sp + 8, 0); // col
bus.write_word(sp + 10, 1); // dataLen
bus.write_long(sp + 12, data_a_ptr);
let set_a = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(set_a.is_some());
assert!(set_a.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 16);
// LSetCell row 1 = "B"
bus.write_bytes(data_b_ptr, b"B");
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0058); // LSetCell
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 1); // row
bus.write_word(sp + 8, 0); // col
bus.write_word(sp + 10, 1); // dataLen
bus.write_long(sp + 12, data_b_ptr);
let set_b = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(set_b.is_some());
assert!(set_b.unwrap().is_ok());
// Delete row 0; row 1 should compact into row 0.
// Pascal order: lHandle deepest under selector, then rowNum, then count.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0024); // LDelRow
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0); // rowNum
bus.write_word(sp + 8, 1); // count
let del = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(del.is_some());
assert!(del.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0038); // LGetCell
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0); // row
bus.write_word(sp + 8, 0); // col
bus.write_long(sp + 10, out_len_ptr); // VAR dataLen
bus.write_long(sp + 14, out_ptr); // dataPtr
bus.write_word(out_len_ptr, 1); // max bytes to copy
let get = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(get.is_some());
assert!(get.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 18);
assert_eq!(bus.read_word(out_len_ptr), 1);
assert_eq!(bus.read_byte(out_ptr), b'B');
let list_ptr = bus.read_long(list_handle);
let data_bounds_bottom = bus.read_word(list_ptr + 76) as i16;
assert_eq!(data_bounds_bottom, 1);
}
// Pack0 / List Manager ($A9E7) — LSetSelect selector $005C and
// LGetSelect selector $003C.
// IM:IV 1986 p. IV-273: selection toggles are driven by
// LSetSelect(setIt,theCell,lHandle), and LGetSelect checks a
// specific cell or searches from a probe cell forward.
#[test]
fn pack0_lsetselect_lgetselect_use_pascal_argument_order_and_selection_state() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let view_rect_ptr = 0x353400u32;
let data_bounds_ptr = 0x353500u32;
let query_cell_ptr = 0x353600u32;
bus.write_word(view_rect_ptr, 0);
bus.write_word(view_rect_ptr + 2, 0);
bus.write_word(view_rect_ptr + 4, 48);
bus.write_word(view_rect_ptr + 6, 80);
bus.write_word(data_bounds_ptr, 0);
bus.write_word(data_bounds_ptr + 2, 0);
bus.write_word(data_bounds_ptr + 4, 3);
bus.write_word(data_bounds_ptr + 6, 1);
bus.write_word(sp, 0x0044); // LNew
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, 0x210000);
bus.write_word(sp + 14, 0);
bus.write_word(sp + 16, 16);
bus.write_word(sp + 18, 32);
bus.write_long(sp + 20, data_bounds_ptr);
bus.write_long(sp + 24, view_rect_ptr);
bus.write_long(sp + 28, 0);
let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(create.is_some());
assert!(create.unwrap().is_ok());
let list_handle = bus.read_long(sp + 28);
// LSetSelect(TRUE, Cell(1,0), hList) with Pascal order:
// lHandle closest to selector, then Cell, then Boolean.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x005C); // LSetSelect
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 1); // cell.v = row
bus.write_word(sp + 8, 0); // cell.h = col
bus.write_word(sp + 10, 0x0100); // setIt = TRUE
let set = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(set.is_some());
assert!(set.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
// LGetSelect(FALSE, &Cell(1,0), hList) should report selected.
bus.write_word(query_cell_ptr, 1);
bus.write_word(query_cell_ptr + 2, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x003C); // LGetSelect
bus.write_long(sp + 2, list_handle);
bus.write_long(sp + 6, query_cell_ptr);
bus.write_word(sp + 10, 0x0000); // next = FALSE
bus.write_word(sp + 12, 0xBEEF);
let get_exact = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(get_exact.is_some());
assert!(get_exact.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0x0100);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
assert_eq!(bus.read_word(query_cell_ptr), 1);
assert_eq!(bus.read_word(query_cell_ptr + 2), 0);
// LGetSelect(TRUE, &Cell(0,0), hList) should advance the probe
// to the selected cell at or after the starting point.
bus.write_word(query_cell_ptr, 0);
bus.write_word(query_cell_ptr + 2, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x003C); // LGetSelect
bus.write_long(sp + 2, list_handle);
bus.write_long(sp + 6, query_cell_ptr);
bus.write_word(sp + 10, 0x0100); // next = TRUE
bus.write_word(sp + 12, 0xBEEF);
let get_next = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(get_next.is_some());
assert!(get_next.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0x0100);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
assert_eq!(bus.read_word(query_cell_ptr), 1);
assert_eq!(bus.read_word(query_cell_ptr + 2), 0);
// LSetSelect(FALSE, Cell(1,0), hList) should clear the cell.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x005C); // LSetSelect
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 1);
bus.write_word(sp + 8, 0);
bus.write_word(sp + 10, 0x00FF); // setIt = FALSE; low byte is padding garbage
let clear = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(clear.is_some());
assert!(clear.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
bus.write_word(query_cell_ptr, 1);
bus.write_word(query_cell_ptr + 2, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x003C); // LGetSelect
bus.write_long(sp + 2, list_handle);
bus.write_long(sp + 6, query_cell_ptr);
bus.write_word(sp + 10, 0x0000); // next = FALSE
bus.write_word(sp + 12, 0xBEEF);
let get_cleared = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(get_cleared.is_some());
assert!(get_cleared.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
}
// Pack0 / List Manager ($A9E7) — LClick selector $0018
// IM:IV 1986 p. IV-273: TRUE on double-click in same cell; LLastClick reports clicked cell.
#[test]
fn pack0_lclick_same_cell_double_click_returns_true_and_lastclick_tracks_cell() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let view_rect_ptr = 0x353000u32;
let data_bounds_ptr = 0x353100u32;
bus.write_word(view_rect_ptr, 0);
bus.write_word(view_rect_ptr + 2, 0);
bus.write_word(view_rect_ptr + 4, 40);
bus.write_word(view_rect_ptr + 6, 80);
bus.write_word(data_bounds_ptr, 0);
bus.write_word(data_bounds_ptr + 2, 0);
bus.write_word(data_bounds_ptr + 4, 2);
bus.write_word(data_bounds_ptr + 6, 1);
bus.write_word(sp, 0x0044); // LNew
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, 0x210000);
bus.write_word(sp + 14, 0);
bus.write_word(sp + 16, 10); // cSize.v
bus.write_word(sp + 18, 20); // cSize.h
bus.write_long(sp + 20, data_bounds_ptr);
bus.write_long(sp + 24, view_rect_ptr);
bus.write_long(sp + 28, 0);
let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(create.is_some());
assert!(create.unwrap().is_ok());
let list_handle = bus.read_long(sp + 28);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0018); // LClick
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0); // modifiers
bus.write_word(sp + 8, 5); // pt.v in row 0
bus.write_word(sp + 10, 5); // pt.h in col 0
bus.write_word(sp + 12, 0xBEEF); // Boolean result
let first = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(first.is_some());
assert!(first.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0018); // LClick
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 5);
bus.write_word(sp + 10, 5);
bus.write_word(sp + 12, 0xBEEF);
let second = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(second.is_some());
assert!(second.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0x0100);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0040); // LLastClick
bus.write_long(sp + 2, list_handle);
bus.write_long(sp + 6, 0xFFFF_FFFF); // result cell placeholder
let last_click = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(last_click.is_some());
assert!(last_click.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0); // row
assert_eq!(bus.read_word(sp + 8), 0); // col
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
#[test]
fn pack0_lclick_miss_clears_lastclick_history() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let view_rect_ptr = 0x355000u32;
let data_bounds_ptr = 0x355100u32;
bus.write_word(view_rect_ptr, 0);
bus.write_word(view_rect_ptr + 2, 0);
bus.write_word(view_rect_ptr + 4, 40);
bus.write_word(view_rect_ptr + 6, 80);
bus.write_word(data_bounds_ptr, 0);
bus.write_word(data_bounds_ptr + 2, 0);
bus.write_word(data_bounds_ptr + 4, 2);
bus.write_word(data_bounds_ptr + 6, 1);
bus.write_word(sp, 0x0044); // LNew
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, 0x210200);
bus.write_word(sp + 14, 0);
bus.write_word(sp + 16, 10); // cSize.v
bus.write_word(sp + 18, 20); // cSize.h
bus.write_long(sp + 20, data_bounds_ptr);
bus.write_long(sp + 24, view_rect_ptr);
bus.write_long(sp + 28, 0);
let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(create.is_some());
assert!(create.unwrap().is_ok());
let list_handle = bus.read_long(sp + 28);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0018); // LClick
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0); // modifiers
bus.write_word(sp + 8, 5); // pt.v in row 0
bus.write_word(sp + 10, 5); // pt.h in col 0
bus.write_word(sp + 12, 0xBEEF);
let first = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(first.is_some());
assert!(first.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0018); // LClick
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 200); // miss
bus.write_word(sp + 10, 200);
bus.write_word(sp + 12, 0xBEEF);
let miss = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(miss.is_some());
assert!(miss.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0040); // LLastClick
bus.write_long(sp + 2, list_handle);
bus.write_long(sp + 6, 0xFFFF_FFFF);
let last_click = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(last_click.is_some());
assert!(last_click.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0xFFFF);
assert_eq!(bus.read_word(sp + 8), 0xFFFF);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0018); // LClick
bus.write_long(sp + 2, list_handle);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 5);
bus.write_word(sp + 10, 5);
bus.write_word(sp + 12, 0xBEEF);
let second = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(second.is_some());
assert!(second.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0);
}
#[test]
fn pack0_llastclick_before_any_click_returns_negative_cell() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let view_rect_ptr = 0x354000u32;
let data_bounds_ptr = 0x354100u32;
bus.write_word(view_rect_ptr, 0);
bus.write_word(view_rect_ptr + 2, 0);
bus.write_word(view_rect_ptr + 4, 40);
bus.write_word(view_rect_ptr + 6, 80);
bus.write_word(data_bounds_ptr, 0);
bus.write_word(data_bounds_ptr + 2, 0);
bus.write_word(data_bounds_ptr + 4, 1);
bus.write_word(data_bounds_ptr + 6, 1);
bus.write_word(sp, 0x0044); // LNew
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, 0x210100);
bus.write_word(sp + 14, 0);
bus.write_word(sp + 16, 10);
bus.write_word(sp + 18, 20);
bus.write_long(sp + 20, data_bounds_ptr);
bus.write_long(sp + 24, view_rect_ptr);
bus.write_long(sp + 28, 0);
let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(create.is_some());
assert!(create.unwrap().is_ok());
let list_handle = bus.read_long(sp + 28);
assert_ne!(list_handle, 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0040); // LLastClick
bus.write_long(sp + 2, list_handle);
bus.write_long(sp + 6, 0xBEEF_BEEF); // result cell placeholder
let last_click = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
assert!(last_click.is_some());
assert!(last_click.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
assert_eq!(bus.read_word(sp + 6), 0xFFFF);
assert_eq!(bus.read_word(sp + 8), 0xFFFF);
}
// Pack1 / List Manager ($A9E8) — LNew selector $0044
// IM:IV 1986 pp. IV-269 to IV-270: LNew returns a live handle and initializes
// selFlags=0 with lActive=TRUE.
#[test]
fn pack1_lnew_returns_non_nil_listhandle_with_default_selection_and_active_flags() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let view_rect_ptr = 0x356000u32;
let data_bounds_ptr = 0x356100u32;
let window_ptr = 0x210800u32;
bus.write_word(view_rect_ptr, 0);
bus.write_word(view_rect_ptr + 2, 0);
bus.write_word(view_rect_ptr + 4, 96);
bus.write_word(view_rect_ptr + 6, 96);
bus.write_word(data_bounds_ptr, 0);
bus.write_word(data_bounds_ptr + 2, 0);
bus.write_word(data_bounds_ptr + 4, 1);
bus.write_word(data_bounds_ptr + 6, 1);
bus.write_word(sp, 0x0044); // LNew
bus.write_word(sp + 2, 0);
bus.write_word(sp + 4, 0);
bus.write_word(sp + 6, 0);
bus.write_word(sp + 8, 0);
bus.write_long(sp + 10, window_ptr);
bus.write_word(sp + 14, 0); // default LDEF
bus.write_word(sp + 16, 16);
bus.write_word(sp + 18, 16);
bus.write_long(sp + 20, data_bounds_ptr);
bus.write_long(sp + 24, view_rect_ptr);
bus.write_long(sp + 28, 0xDEAD_BEEF);
let result = disp.dispatch_toolbox(true, 0x1E8, &mut cpu, &mut bus);
assert!(result.is_some(), "Pack1 should be handled");
assert!(result.unwrap().is_ok(), "Pack1 should return");
assert_eq!(cpu.read_reg(Register::A7), sp + 28);
let list_handle = bus.read_long(sp + 28);
assert_ne!(list_handle, 0);
let list_ptr = bus.read_long(list_handle);
assert_ne!(list_ptr, 0);
assert_eq!(
bus.read_byte(list_ptr + 36),
0,
"selFlags should default to 0"
);
assert_eq!(
bus.read_byte(list_ptr + 37),
1,
"lActive should default to TRUE"
);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0028); // LDispose
bus.write_long(sp + 2, list_handle);
let dispose = disp.dispatch_toolbox(true, 0x1E8, &mut cpu, &mut bus);
assert!(dispose.is_some(), "Pack1 should be handled");
assert!(dispose.unwrap().is_ok(), "Pack1 should return");
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
assert!(!disp.list_states.contains_key(&list_handle));
}
// Pack1 / List Manager ($A9E8) — LSearch selector $0054
// IM:IV 1986 p. IV-274: nil-list fallback returns FALSE and leaves the probe cell unchanged.
#[test]
fn pack1_lsearch_nil_list_returns_false_and_preserves_probe_cell() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let probe_ptr = 0x357000u32;
let data_ptr = 0x357100u32;
bus.write_word(probe_ptr, 0x1122);
bus.write_word(probe_ptr + 2, 0x3344);
bus.write_bytes(data_ptr, b"Rose");
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0054); // LSearch
bus.write_long(sp + 2, 0); // nil list handle
bus.write_long(sp + 6, probe_ptr);
bus.write_long(sp + 10, 0xDEAD_BEEF); // bogus callback pointer, must not be entered
bus.write_word(sp + 14, 4);
bus.write_long(sp + 16, data_ptr);
bus.write_word(sp + 20, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x1E8, &mut cpu, &mut bus);
assert!(result.is_some(), "Pack1 should be handled");
assert!(result.unwrap().is_ok(), "Pack1 should return");
assert_eq!(cpu.read_reg(Register::A7), sp + 20);
assert_eq!(bus.read_word(sp + 20), 0);
assert_eq!(bus.read_word(probe_ptr), 0x1122);
assert_eq!(bus.read_word(probe_ptr + 2), 0x3344);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// PackBits / UnpackBits ($A8CF / $A8D0)
// Inside Macintosh Volume I (1985), p. I-470.
#[test]
fn packbits_compresses_runs_and_advances_var_pointers() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src_ptr_ptr = 0x320000u32;
let dst_ptr_ptr = 0x320004u32;
let src_data = 0x320100u32;
let dst_data = 0x320200u32;
bus.write_bytes(src_data, &[0xAA, 0xAA, 0xAA, 0xAA]);
bus.write_long(src_ptr_ptr, src_data);
bus.write_long(dst_ptr_ptr, dst_data);
bus.write_word(sp, 4); // srcBytes
bus.write_long(sp + 2, dst_ptr_ptr); // VAR dstPtr
bus.write_long(sp + 6, src_ptr_ptr); // VAR srcPtr
let result = disp.dispatch_toolbox(true, 0x0CF, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(bus.read_long(src_ptr_ptr), src_data + 4);
assert_eq!(bus.read_long(dst_ptr_ptr), dst_data + 2);
assert_eq!(bus.read_byte(dst_data), 0xFD); // -(4-1)
assert_eq!(bus.read_byte(dst_data + 1), 0xAA);
}
#[test]
fn packbits_literal_sequence_emits_literal_packet_and_pops_10_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src_ptr_ptr = 0x321000u32;
let dst_ptr_ptr = 0x321004u32;
let src_data = 0x321100u32;
let dst_data = 0x321200u32;
bus.write_bytes(src_data, &[0x10, 0x20, 0x30]);
bus.write_long(src_ptr_ptr, src_data);
bus.write_long(dst_ptr_ptr, dst_data);
bus.write_word(sp, 3); // srcBytes
bus.write_long(sp + 2, dst_ptr_ptr); // VAR dstPtr
bus.write_long(sp + 6, src_ptr_ptr); // VAR srcPtr
let result = disp.dispatch_toolbox(true, 0x0CF, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(bus.read_long(src_ptr_ptr), src_data + 3);
assert_eq!(bus.read_long(dst_ptr_ptr), dst_data + 4);
assert_eq!(bus.read_byte(dst_data), 0x02); // literal len 3 => flag 2
assert_eq!(bus.read_byte(dst_data + 1), 0x10);
assert_eq!(bus.read_byte(dst_data + 2), 0x20);
assert_eq!(bus.read_byte(dst_data + 3), 0x30);
}
#[test]
fn unpackbits_expands_packbits_stream_and_advances_var_pointers() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src_ptr_ptr = 0x322000u32;
let dst_ptr_ptr = 0x322004u32;
let src_data = 0x322100u32;
let dst_data = 0x322200u32;
// Encodes bytes: [AA, AA, AA, AA, 55]
// repeat packet (0xFD, 0xAA) + literal packet (0x00, 0x55).
bus.write_bytes(src_data, &[0xFD, 0xAA, 0x00, 0x55]);
bus.write_long(src_ptr_ptr, src_data);
bus.write_long(dst_ptr_ptr, dst_data);
bus.write_word(sp, 5); // dstBytes
bus.write_long(sp + 2, dst_ptr_ptr); // VAR dstPtr
bus.write_long(sp + 6, src_ptr_ptr); // VAR srcPtr
let result = disp.dispatch_toolbox(true, 0x0D0, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(bus.read_long(src_ptr_ptr), src_data + 4);
assert_eq!(bus.read_long(dst_ptr_ptr), dst_data + 5);
assert_eq!(
bus.read_bytes(dst_data, 5),
vec![0xAA, 0xAA, 0xAA, 0xAA, 0x55]
);
}
// System 7+ Dispatch Manager stubs
// Inside Macintosh: More Macintosh Toolbox (1993),
// pp. 6-6/6-29/6-98, 5-18/5-71, and 7-12/7-66.
#[test]
fn componentdispatch_call_path_pops_selector_instance_and_args_and_returns_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0); // component call path
// Inline component-call glue pushes [paramSize:callNum] at SP.
bus.write_long(sp, 0x0004_0000);
bus.write_long(sp + 4, 0x00C0_FFEE); // ComponentInstance
bus.write_long(sp + 8, 0xA5A5_5A5A); // sentinel argument data
let result = disp.dispatch_toolbox(true, 0x02A, &mut cpu, &mut bus);
assert!(result.is_some(), "ComponentDispatch should be handled");
assert!(result.unwrap().is_ok(), "ComponentDispatch should return");
assert_eq!(cpu.read_reg(Register::D0), 0, "stub should return noErr");
assert_eq!(
cpu.read_reg(Register::A7),
sp + 12,
"component call path should consume selector + instance + args"
);
assert_eq!(bus.read_long(sp), 0x0004_0000);
assert_eq!(bus.read_long(sp + 4), 0x00C0_FFEE);
assert_eq!(bus.read_long(sp + 8), 0xA5A5_5A5A);
}
#[test]
fn componentdispatch_internal_request_preserves_non_d0_registers_and_stack() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D0, 0xFFFF_1234); // manager-internal call path
cpu.write_reg(Register::D1, 0x1111_2222);
cpu.write_reg(Register::A0, 0x3333_4444);
cpu.write_reg(Register::A1, 0x5555_6666);
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_toolbox(true, 0x02A, &mut cpu, &mut bus);
assert!(result.is_some(), "ComponentDispatch should be handled");
assert!(result.unwrap().is_ok(), "ComponentDispatch should return");
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::D1), 0x1111_2222);
assert_eq!(cpu.read_reg(Register::A0), 0x3333_4444);
assert_eq!(cpu.read_reg(Register::A1), 0x5555_6666);
assert_eq!(cpu.read_reg(Register::A7), sp_before);
}
#[test]
fn textservicesdispatch_returns_noerr_and_preserves_stack_pointer_in_noop_path() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D0, 0x1234_5678);
cpu.write_reg(Register::D1, 0x1111_2222);
cpu.write_reg(Register::A0, 0x3333_4444);
cpu.write_reg(Register::A1, 0x5555_6666);
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_toolbox(true, 0x254, &mut cpu, &mut bus);
assert!(result.is_some(), "TextServicesDispatch should be handled");
assert!(result.unwrap().is_ok(), "TextServicesDispatch should succeed");
assert_eq!(
cpu.read_reg(Register::D0),
0,
"TextServicesDispatch should return noErr"
);
assert_eq!(cpu.read_reg(Register::D1), 0x1111_2222);
assert_eq!(cpu.read_reg(Register::A0), 0x3333_4444);
assert_eq!(cpu.read_reg(Register::A1), 0x5555_6666);
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"TextServicesDispatch should preserve the caller stack pointer"
);
}
#[test]
fn puticon_preserves_a7() {
// PutIcon is an undocumented internal trap. The conservative HLE stub is
// a no-op, so the proof checks that it preserves A7 and returns cleanly.
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = TEST_SP;
cpu.write_reg(Register::A7, sp_before);
bus.write_long(sp_before, 0x1122_3344);
bus.write_word(sp_before + 4, 0x5566);
let result = disp.dispatch_toolbox(true, 0x1CA, &mut cpu, &mut bus);
assert!(result.is_some(), "PutIcon should be handled");
assert!(result.unwrap().is_ok(), "PutIcon should return");
assert_eq!(
cpu.read_reg(Register::A7),
sp_before,
"PutIcon should preserve A7"
);
assert_eq!(
bus.read_long(sp_before),
0x1122_3344,
"PutIcon should not modify the caller's stack word"
);
assert_eq!(
bus.read_word(sp_before + 4),
0x5566,
"PutIcon should not modify the trailing stack halfword"
);
}
#[test]
fn pack13_initdbpack_selector_returns_noerr_and_preserves_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x0000_0100); // InitDBPack selector
bus.write_long(sp, 0x1122_3344); // sentinel stack word
let result = disp.dispatch_toolbox(true, 0x02F, &mut cpu, &mut bus);
assert!(result.is_some(), "Pack13 should be handled");
assert!(result.unwrap().is_ok(), "Pack13 should return");
assert_eq!(
cpu.read_reg(Register::D0),
0,
"selector 0x0100 should return noErr"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp,
"Pack13 selector-only call should preserve A7"
);
assert_eq!(
bus.read_long(sp),
0x1122_3344,
"Pack13 selector-only call should not touch the stack frame"
);
}
#[test]
fn pack9_aliases_stackspace_and_preserves_stack_slot() {
let (mut toolbox_disp, mut toolbox_cpu, mut toolbox_bus) = setup();
let (mut memory_disp, mut memory_cpu, mut memory_bus) = setup();
let sp = TEST_SP;
toolbox_cpu.write_reg(Register::A7, sp);
memory_cpu.write_reg(Register::A7, sp);
toolbox_bus.write_long(sp, 0x1122_3344);
memory_bus.write_long(sp, 0x1122_3344);
let toolbox_result = toolbox_disp.dispatch_toolbox(true, 0x02B, &mut toolbox_cpu, &mut toolbox_bus);
let memory_result = memory_disp.dispatch_memory(false, 0x65, &mut memory_cpu, &mut memory_bus);
assert!(toolbox_result.is_some(), "Pack9 should be handled");
assert!(toolbox_result.unwrap().is_ok(), "Pack9 should return");
assert!(memory_result.is_some(), "StackSpace should be handled");
assert!(memory_result.unwrap().is_ok(), "StackSpace should return");
assert_eq!(
toolbox_cpu.read_reg(Register::D0),
memory_cpu.read_reg(Register::D0),
"Pack9 should return the same D0 value as StackSpace"
);
assert_eq!(
toolbox_cpu.read_reg(Register::A7),
sp,
"Pack9 should preserve the caller's stack pointer"
);
assert_eq!(
toolbox_bus.read_long(sp),
0x1122_3344,
"Pack9 should not write a Pascal result slot on the caller stack"
);
assert_eq!(
memory_cpu.read_reg(Register::A7),
sp,
"StackSpace should preserve the caller's stack pointer"
);
assert_eq!(
memory_bus.read_long(sp),
0x1122_3344,
"StackSpace should not write a Pascal result slot on the caller stack"
);
}
#[test]
fn pack10_aliases_newemptyhandle_and_preserves_stack_slot() {
let (mut toolbox_disp, mut toolbox_cpu, mut toolbox_bus) = setup();
let (mut memory_disp, mut memory_cpu, mut memory_bus) = setup();
let sp = TEST_SP;
toolbox_cpu.write_reg(Register::A7, sp);
memory_cpu.write_reg(Register::A7, sp);
toolbox_bus.write_long(sp, 0x1122_3344);
memory_bus.write_long(sp, 0x1122_3344);
let toolbox_result = toolbox_disp.dispatch_toolbox(true, 0x02C, &mut toolbox_cpu, &mut toolbox_bus);
let memory_result = memory_disp.dispatch_memory(false, 0x66, &mut memory_cpu, &mut memory_bus);
assert!(toolbox_result.is_some(), "Pack10 should be handled");
assert!(toolbox_result.unwrap().is_ok(), "Pack10 should return");
assert!(memory_result.is_some(), "NewEmptyHandle should be handled");
assert!(memory_result.unwrap().is_ok(), "NewEmptyHandle should return");
assert_eq!(
toolbox_cpu.read_reg(Register::D0),
memory_cpu.read_reg(Register::D0),
"Pack10 should return the same D0 value as NewEmptyHandle"
);
assert_eq!(
toolbox_cpu.read_reg(Register::A7),
sp,
"Pack10 should preserve the caller's stack pointer"
);
assert_eq!(
toolbox_bus.read_long(sp),
0x1122_3344,
"Pack10 should not write a Pascal result slot on the caller stack"
);
assert_ne!(
toolbox_cpu.read_reg(Register::A0),
0,
"Pack10 should return a non-NIL handle in A0"
);
assert_eq!(
toolbox_bus.read_long(toolbox_cpu.read_reg(Register::A0)),
0,
"Pack10 should initialize the returned master pointer to NIL"
);
assert_ne!(
memory_cpu.read_reg(Register::A0),
0,
"NewEmptyHandle should return a non-NIL handle in A0"
);
assert_eq!(
memory_bus.read_long(memory_cpu.read_reg(Register::A0)),
0,
"NewEmptyHandle should initialize the returned master pointer to NIL"
);
assert_eq!(
memory_cpu.read_reg(Register::A7),
sp,
"NewEmptyHandle should preserve the caller's stack pointer"
);
assert_eq!(
memory_bus.read_long(sp),
0x1122_3344,
"NewEmptyHandle should not write a Pascal result slot on the caller stack"
);
}
#[test]
fn icondispatch_selector_zero_returns_noerr_and_pops_eight_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x0000_0000); // selector 0 (NewIconSuite / no-op stub path)
bus.write_long(sp, 0x1122_3344);
bus.write_long(sp + 4, 0x5566_7788);
let result = disp.dispatch_toolbox(true, 0x3C9, &mut cpu, &mut bus);
assert!(result.is_some(), "IconDispatch should be handled");
assert!(result.unwrap().is_ok(), "IconDispatch should return");
assert_eq!(
cpu.read_reg(Register::D0),
0,
"selector 0 should return noErr"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp + 8,
"selector 0 should pop eight bytes"
);
assert_eq!(bus.read_long(sp), 0x1122_3344);
assert_eq!(bus.read_long(sp + 4), 0x5566_7788);
}
#[test]
fn icondispatch_unsupported_selector_returns_param_err_and_pops_eight_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x0000_050B); // representative unsupported selector
bus.write_long(sp, 0x1122_3344);
bus.write_long(sp + 4, 0x5566_7788);
let result = disp.dispatch_toolbox(true, 0x3C9, &mut cpu, &mut bus);
assert!(result.is_some(), "IconDispatch should be handled");
assert!(result.unwrap().is_ok(), "IconDispatch should return");
assert_eq!(
cpu.read_reg(Register::D0) as i16,
-50,
"stub should return paramErr"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp + 8,
"unsupported selector should still pop eight bytes"
);
assert_eq!(bus.read_long(sp), 0x1122_3344);
assert_eq!(bus.read_long(sp + 4), 0x5566_7788);
}
#[test]
fn icondispatch_selector_zero_preserves_non_d0_registers_and_pops_eight_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D1, 0x2222_3333);
cpu.write_reg(Register::A0, 0x4444_5555);
cpu.write_reg(Register::A1, 0x6666_7777);
let sp_before = cpu.read_reg(Register::A7);
cpu.write_reg(Register::D0, 0x0000_0000);
let result = disp.dispatch_toolbox(true, 0x3C9, &mut cpu, &mut bus);
assert!(result.is_some(), "IconDispatch should be handled");
assert!(result.unwrap().is_ok(), "IconDispatch should return");
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::D1), 0x2222_3333);
assert_eq!(cpu.read_reg(Register::A0), 0x4444_5555);
assert_eq!(cpu.read_reg(Register::A1), 0x6666_7777);
assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
}
#[test]
fn translationdispatch_selector_zero_returns_param_err_and_pops_four_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x0000_001C); // GetFileTypesThatAppCanNativelyOpen
bus.write_long(sp, 0x99AA_BBCC);
bus.write_long(sp + 4, 0xDDEE_F011);
let result = disp.dispatch_toolbox(true, 0x3FC, &mut cpu, &mut bus);
assert!(result.is_some(), "TranslationDispatch should be handled");
assert!(result.unwrap().is_ok(), "TranslationDispatch should return");
assert_eq!(
cpu.read_reg(Register::D0) as i16,
-50,
"stub should return paramErr"
);
assert_eq!(
cpu.read_reg(Register::A7),
sp + 4,
"stub should pop four bytes"
);
assert_eq!(bus.read_long(sp), 0x99AA_BBCC);
assert_eq!(bus.read_long(sp + 4), 0xDDEE_F011);
}
#[test]
fn translationdispatch_selector_zero_preserves_non_d0_registers_and_pops_four_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::D0, 0x0000_0009);
cpu.write_reg(Register::D1, 0x7777_8888);
cpu.write_reg(Register::A0, 0x9999_AAAA);
cpu.write_reg(Register::A1, 0xBBBB_CCCC);
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_toolbox(true, 0x3FC, &mut cpu, &mut bus);
assert!(result.is_some(), "TranslationDispatch should be handled");
assert!(result.unwrap().is_ok(), "TranslationDispatch should return");
assert_eq!(cpu.read_reg(Register::D0) as i16, -50);
assert_eq!(cpu.read_reg(Register::D1), 0x7777_8888);
assert_eq!(cpu.read_reg(Register::A0), 0x9999_AAAA);
assert_eq!(cpu.read_reg(Register::A1), 0xBBBB_CCCC);
assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
}
#[test]
fn threaddispatch_begin_and_end_critical_roundtrip() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0xBEEF);
cpu.write_reg(Register::D0, 0x0000_000B);
let begin = disp.dispatch_toolbox(true, 0x3F2, &mut cpu, &mut bus);
assert!(
begin.is_some(),
"ThreadDispatch should handle ThreadBeginCritical"
);
assert!(begin.unwrap().is_ok(), "ThreadBeginCritical should return");
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
assert_eq!(bus.read_word(sp), 0);
assert_eq!(disp.thread_critical_nesting, 1);
cpu.write_reg(Register::D0, 0x0000_000C);
let end = disp.dispatch_toolbox(true, 0x3F2, &mut cpu, &mut bus);
assert!(
end.is_some(),
"ThreadDispatch should handle ThreadEndCritical"
);
assert!(end.unwrap().is_ok(), "ThreadEndCritical should return");
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::A7), sp);
assert_eq!(bus.read_word(sp), 0);
assert_eq!(disp.thread_critical_nesting, 0);
}
#[test]
fn threaddispatch_endcritical_underflow_returns_thread_protocol_err() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0xBEEF);
cpu.write_reg(Register::D0, 0x0000_000C);
let result = disp.dispatch_toolbox(true, 0x3F2, &mut cpu, &mut bus);
assert!(
result.is_some(),
"ThreadDispatch should handle ThreadEndCritical"
);
assert!(result.unwrap().is_ok(), "ThreadEndCritical should return");
assert_eq!(cpu.read_reg(Register::D0) as i16, -619);
assert_eq!(cpu.read_reg(Register::A7), sp);
assert_eq!(bus.read_word(sp), (-619i16) as u16);
assert_eq!(disp.thread_critical_nesting, 0);
}
#[test]
fn threaddispatch_unsupported_selector_returns_param_err() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = cpu.read_reg(Register::A7);
bus.write_word(sp, 0xBEEF);
cpu.write_reg(Register::D0, 0x0000_0000);
cpu.write_reg(Register::D1, 0x1111_2222);
cpu.write_reg(Register::A0, 0x3333_4444);
cpu.write_reg(Register::A1, 0x5555_6666);
let result = disp.dispatch_toolbox(true, 0x3F2, &mut cpu, &mut bus);
assert!(result.is_some(), "ThreadDispatch should be handled");
assert!(result.unwrap().is_ok(), "ThreadDispatch should return");
assert_eq!(cpu.read_reg(Register::D0) as i16, -50);
assert_eq!(cpu.read_reg(Register::D1), 0x1111_2222);
assert_eq!(cpu.read_reg(Register::A0), 0x3333_4444);
assert_eq!(cpu.read_reg(Register::A1), 0x5555_6666);
assert_eq!(cpu.read_reg(Register::A7), sp);
assert_eq!(bus.read_word(sp), (-50i16) as u16);
assert_eq!(disp.thread_critical_nesting, 0);
}
// DictionaryDispatch ($AA53)
// Inside Macintosh: Text (1993), pp. 8-11, 8-21 to 8-24, 8-34.
// InitializeDictionary is the public client call that creates the
// internal B*-tree for a freshly created dictionary file. The 68K
// Pascal wrapper pushes a 10-byte argument frame:
// FSSpecPtr (4) + maximumKeyLength (2) + keyAttributes (2) + script (2)
#[test]
fn dictionarydispatch_initialize_dictionary_selector_pops_ten_byte_frame_and_returns_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let dict_spec = bus.alloc(64);
cpu.write_reg(Register::D0, 0x0500);
bus.write_long(sp, dict_spec);
bus.write_word(sp + 4, 129);
bus.write_word(sp + 6, 0x0010);
bus.write_word(sp + 8, 0x0000);
bus.write_word(sp + 10, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x253, &mut cpu, &mut bus);
assert!(result.is_some(), "DictionaryDispatch should be handled");
assert!(
result.unwrap().is_ok(),
"InitializeDictionary should return"
);
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(bus.read_word(sp + 10), 0);
}
// Pack2 / DiskInit ($A9E9)
// Inside Macintosh: Files (1992), pp. 5-15 to 5-21 and p. 5-24.
#[test]
fn pack2_dibadmount_selector_0000_returns_zero_and_pops_selector_plus_point_and_evtmessage() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0000); // DIBadMount
bus.write_word(sp + 2, 12); // where.v
bus.write_word(sp + 4, 34); // where.h
bus.write_long(sp + 6, 0x1122_3344); // evtMessage
bus.write_word(sp + 10, 0xBEEF); // Integer result slot
let result = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 10), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
#[test]
fn pack2_diload_and_diunload_selectors_pop_selector_only_and_return_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0002); // DILoad
bus.write_word(sp + 2, 0xA55A); // sentinel after selector
let sp_pre_load = cpu.read_reg(Register::A7);
let load = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
let sp_post_load = cpu.read_reg(Register::A7);
assert!(load.is_some());
assert!(load.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(sp_post_load, sp_pre_load + 2);
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(sp + 2), 0xA55A);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0004); // DIUnload
bus.write_word(sp + 2, 0x5AA5); // sentinel after selector
let sp_pre_unload = cpu.read_reg(Register::A7);
let unload = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
let sp_post_unload = cpu.read_reg(Register::A7);
assert!(unload.is_some());
assert!(unload.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(sp_post_unload, sp_pre_unload + 2);
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(bus.read_word(sp + 2), 0x5AA5);
}
#[test]
fn pack2_diformat_and_diverify_selectors_return_noerr_and_pop_drvnum_argument() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0006); // DIFormat
bus.write_word(sp + 2, 7); // drvNum
bus.write_word(sp + 4, 0xBEEF); // OSErr result slot
let format = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(format.is_some());
assert!(format.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(cpu.read_reg(Register::D0), 0);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0008); // DIVerify
bus.write_word(sp + 2, 7); // drvNum
bus.write_word(sp + 4, 0xBEEF); // OSErr result slot
let verify = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(verify.is_some());
assert!(verify.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
#[test]
fn pack2_dizero_selector_000a_consumes_pascal_str255_by_value_and_returns_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x000A); // DIZero
bus.write_word(sp + 2, 3); // drvNum
bus.write_byte(sp + 4, 5); // Str255 length
bus.write_bytes(sp + 5, b"Disk0");
bus.write_word(sp + 260, 0xBEEF); // OSErr result slot
let result = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 260), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 260);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
#[test]
fn pack2_selector_table_integrated_contract_covers_all_documented_selector_frames() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// DIBadMount: selector + Point + evtMessage + result slot.
bus.write_word(sp, 0x0000);
bus.write_word(sp + 2, 12);
bus.write_word(sp + 4, 34);
bus.write_long(sp + 6, 0x1122_3344);
bus.write_word(sp + 10, 0xBEEF);
let bad_mount = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(bad_mount.is_some());
assert!(bad_mount.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 10), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
// DILoad / DIUnload: selector only.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0002);
bus.write_word(sp + 2, 0xA55A);
let load = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(load.is_some());
assert!(load.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(bus.read_word(sp + 2), 0xA55A);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0004);
bus.write_word(sp + 2, 0x5AA5);
let unload = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(unload.is_some());
assert!(unload.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(bus.read_word(sp + 2), 0x5AA5);
// DIFormat / DIVerify: selector + drvNum + result slot.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0006);
bus.write_word(sp + 2, 7);
bus.write_word(sp + 4, 0xBEEF);
let format = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(format.is_some());
assert!(format.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x0008);
bus.write_word(sp + 2, 7);
bus.write_word(sp + 4, 0xBEEF);
let verify = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(verify.is_some());
assert!(verify.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
// DIZero: selector + drvNum + by-value Str255 + result slot.
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 0x000A);
bus.write_word(sp + 2, 3);
bus.write_byte(sp + 4, 5);
bus.write_bytes(sp + 5, b"Disk0");
bus.write_word(sp + 260, 0xBEEF);
let zero = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
assert!(zero.is_some());
assert!(zero.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 260), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 260);
}
// Pack3 / Standard File ($A9EA) — StandardGetFile selector $0006
// IM:Files 1992 pp. 3-50 and 3-61: cancel sets sfGood to FALSE.
#[test]
fn standard_get_file_cancel_sets_sfgood_false_and_pops_16_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let reply_ptr = 0x320000u32;
bus.write_byte(reply_ptr, 0xFF);
bus.write_byte(reply_ptr + 1, 0xAB);
bus.write_word(sp, 0x0006); // StandardGetFile selector
bus.write_long(sp + 2, reply_ptr); // VAR reply
let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_byte(reply_ptr), 0);
assert_eq!(bus.read_byte(reply_ptr + 1), 0xAB);
assert_eq!(cpu.read_reg(Register::A7), sp + 16);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// Pack3 / Standard File ($A9EA) — SFGetFile selector $0002
// IM:Files 1992 pp. 3-53 and 3-61; IM:I I-523..I-526.
#[test]
fn sf_get_file_cancel_sets_good_false_and_pops_28_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let reply_ptr = 0x320100u32;
bus.write_byte(reply_ptr, 0xFF);
bus.write_word(sp, 0x0002); // SFGetFile selector
bus.write_long(sp + 2, reply_ptr); // VAR reply
let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_byte(reply_ptr), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 28);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// Pack3 / Standard File ($A9EA) — CustomGetFile selector $0008
// IM:Files 1992 pp. 3-51..3-54: reply pointer is still the VAR output.
#[test]
fn custom_get_file_cancel_uses_reply_pointer_at_sp_plus_28_and_pops_42_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let decoy_reply_ptr = 0x320180u32;
let actual_reply_ptr = 0x320200u32;
bus.write_byte(decoy_reply_ptr, 0xEE);
bus.write_byte(actual_reply_ptr, 0xFF);
bus.write_word(sp, 0x0008); // CustomGetFile selector
bus.write_long(sp + 2, decoy_reply_ptr); // should be ignored
bus.write_long(sp + 28, actual_reply_ptr); // VAR reply for selector $0008
let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_byte(decoy_reply_ptr), 0xEE);
assert_eq!(bus.read_byte(actual_reply_ptr), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 42);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// Pack6 / Intl Utilities ($A9ED) — IUMetric selector $0004
// IM:I I-505: returns TRUE when metric, otherwise FALSE.
#[test]
fn iumetric_returns_false_in_result_slot_and_pops_selector_only() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0004); // IUMetric selector
bus.write_word(sp + 2, 0xFFFF); // Boolean result slot
let result = disp.dispatch_toolbox(true, 0x1ED, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 2), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// Pack6 / Intl Utilities ($A9ED) — IUMagIDString selector $000C
// IM:I I-507: returns 0 for equal (ignoring secondary ordering), 1 otherwise.
#[test]
fn iumagidstring_case_insensitive_equal_returns_zero_and_pops_14_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let a_ptr = 0x330000u32;
let b_ptr = 0x330100u32;
bus.write_bytes(a_ptr, b"Rose");
bus.write_bytes(b_ptr, b"rOsE");
bus.write_word(sp, 0x000C); // IUMagIDString selector
bus.write_word(sp + 2, 4); // bLen
bus.write_word(sp + 4, 4); // aLen
bus.write_long(sp + 6, b_ptr);
bus.write_long(sp + 10, a_ptr);
bus.write_word(sp + 14, 0xFFFF); // result slot
let result = disp.dispatch_toolbox(true, 0x1ED, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 14), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 14);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
#[test]
fn iumagidstring_case_insensitive_mismatch_returns_one_and_pops_14_bytes() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let a_ptr = 0x330200u32;
let b_ptr = 0x330300u32;
bus.write_bytes(a_ptr, b"Rose");
bus.write_bytes(b_ptr, b"Moss");
bus.write_word(sp, 0x000C); // IUMagIDString selector
bus.write_word(sp + 2, 4); // bLen
bus.write_word(sp + 4, 4); // aLen
bus.write_long(sp + 6, b_ptr);
bus.write_long(sp + 10, a_ptr);
bus.write_word(sp + 14, 0xFFFF); // result slot
let result = disp.dispatch_toolbox(true, 0x1ED, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 14), 1);
assert_eq!(cpu.read_reg(Register::A7), sp + 14);
assert_eq!(cpu.read_reg(Register::D0), 1);
}
// Pack12 / Color Picker ($A82E)
// Inside Macintosh Volume VI (1991), pp. 19-10 to 19-13.
#[test]
fn pack12_fix2smallfract_selector_0001_returns_low_word_and_pops_selector_plus_fixed() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0001); // Fix2SmallFract selector
bus.write_long(sp + 2, 0x0000_5678); // Fixed input in 0..1 range
bus.write_word(sp + 6, 0xBEEF); // SmallFract result slot
let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 6), 0x5678);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
#[test]
fn pack12_smallfract2fix_selector_0002_returns_low_word_fixed_and_pops_selector_plus_smallfract(
) {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0002); // SmallFract2Fix selector
bus.write_word(sp + 2, 0x89AB); // SmallFract input
bus.write_long(sp + 4, 0xDEAD_BEEF); // Fixed result slot
let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x0000_89AB);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
#[test]
fn pack12_getcolor_selector_0009_returns_false_and_pops_selector_plus_arguments() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let out_ptr = 0x350000u32;
let in_ptr = 0x350100u32;
bus.write_word(in_ptr, 0x1234);
bus.write_word(in_ptr + 2, 0x5678);
bus.write_word(in_ptr + 4, 0x9ABC);
bus.write_word(out_ptr, 0x1234);
bus.write_word(out_ptr + 2, 0x5678);
bus.write_word(out_ptr + 4, 0x9ABC);
bus.write_word(sp, 0x0009); // GetColor selector
bus.write_long(sp + 2, out_ptr); // outColor pointer
bus.write_long(sp + 6, in_ptr); // inColor pointer
bus.write_long(sp + 10, 0x350200); // prompt pointer
bus.write_long(sp + 14, 0x000A_0014); // Point(where)
bus.write_word(sp + 18, 0xFFFF); // Boolean result slot
let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(out_ptr), 0x1234);
assert_eq!(bus.read_word(out_ptr + 2), 0x5678);
assert_eq!(bus.read_word(out_ptr + 4), 0x9ABC);
assert_eq!(bus.read_word(sp + 18), 0);
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 18);
}
#[test]
fn pack12_rgb2hsl_selector_0006_converts_pure_red_to_expected_hsl_words() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src_ptr = 0x350000u32;
let dst_ptr = 0x350100u32;
bus.write_word(src_ptr, 0xFFFF);
bus.write_word(src_ptr + 2, 0x0000);
bus.write_word(src_ptr + 4, 0x0000);
bus.write_word(dst_ptr, 0xDEAD);
bus.write_word(dst_ptr + 2, 0xBEEF);
bus.write_word(dst_ptr + 4, 0xCAFE);
bus.write_word(sp, 0x0006); // RGB2HSL selector
bus.write_long(sp + 2, dst_ptr);
bus.write_long(sp + 6, src_ptr);
let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(dst_ptr), 0x0000);
assert_eq!(bus.read_word(dst_ptr + 2), 0xFFFF);
assert_eq!(bus.read_word(dst_ptr + 4), 0x8000);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
#[test]
fn pack12_hsv2rgb_selector_0007_converts_pure_red_hsv_to_rgb_words() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src_ptr = 0x350000u32;
let dst_ptr = 0x350100u32;
bus.write_word(src_ptr, 0x0000);
bus.write_word(src_ptr + 2, 0xFFFF);
bus.write_word(src_ptr + 4, 0xFFFF);
bus.write_word(dst_ptr, 0xDEAD);
bus.write_word(dst_ptr + 2, 0xBEEF);
bus.write_word(dst_ptr + 4, 0xCAFE);
bus.write_word(sp, 0x0007); // HSV2RGB selector
bus.write_long(sp + 2, dst_ptr);
bus.write_long(sp + 6, src_ptr);
let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(dst_ptr), 0xFFFF);
assert_eq!(bus.read_word(dst_ptr + 2), 0x0000);
assert_eq!(bus.read_word(dst_ptr + 4), 0x0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
#[test]
fn pack12_rgb2hsv_selector_0008_converts_pure_red_to_expected_hsv_words() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src_ptr = 0x350000u32;
let dst_ptr = 0x350100u32;
bus.write_word(src_ptr, 0xFFFF);
bus.write_word(src_ptr + 2, 0x0000);
bus.write_word(src_ptr + 4, 0x0000);
bus.write_word(dst_ptr, 0xDEAD);
bus.write_word(dst_ptr + 2, 0xBEEF);
bus.write_word(dst_ptr + 4, 0xCAFE);
bus.write_word(sp, 0x0008); // RGB2HSV selector
bus.write_long(sp + 2, dst_ptr);
bus.write_long(sp + 6, src_ptr);
let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(dst_ptr), 0x0000);
assert_eq!(bus.read_word(dst_ptr + 2), 0xFFFF);
assert_eq!(bus.read_word(dst_ptr + 4), 0xFFFF);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
#[test]
fn pack12_cmy2rgb_selector_0003_converts_each_smallfract_channel_to_complementary_rgb_word() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src_ptr = 0x350000u32;
let dst_ptr = 0x350100u32;
bus.write_word(src_ptr, 0x1111);
bus.write_word(src_ptr + 2, 0x2222);
bus.write_word(src_ptr + 4, 0x3333);
bus.write_word(dst_ptr, 0xDEAD);
bus.write_word(dst_ptr + 2, 0xBEEF);
bus.write_word(dst_ptr + 4, 0xCAFE);
bus.write_word(sp, 0x0003); // CMY2RGB selector
bus.write_long(sp + 2, dst_ptr);
bus.write_long(sp + 6, src_ptr);
let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(dst_ptr), 0xEEEE);
assert_eq!(bus.read_word(dst_ptr + 2), 0xDDDD);
assert_eq!(bus.read_word(dst_ptr + 4), 0xCCCC);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
// Pack14 / Help Manager ($A830)
// Public MPW declarations: HelpMgr.h selector-trap shims
// HMGetHelpMenuHandle(VAR mh: MenuHandle): OSErr;
// HMGetFont(VAR font: Integer): OSErr;
// Inside Macintosh: More Macintosh Toolbox 1993, pp. 3-109 to 3-111;
// selector table p. 3-173.
#[test]
fn pack14_hmgethelpmenuhandle_writes_nil_and_returns_hmhelpmanagernotinited() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let mh_ptr = bus.alloc(4);
bus.write_long(mh_ptr, 0xDEAD_BEEF);
bus.write_word(sp, 0x0200); // HMGetHelpMenuHandle selector
bus.write_long(sp + 2, mh_ptr);
bus.write_word(sp + 6, 0xBEEF); // result slot poison
let result = disp.dispatch_toolbox(true, 0x030, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(mh_ptr), 0);
assert_eq!(bus.read_word(sp + 6), (-855i16) as u16);
assert_eq!(cpu.read_reg(Register::D0), (-855i16) as i32 as u32);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
#[test]
fn pack14_hmgetfont_writes_zero_and_returns_noerr() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let font_ptr = bus.alloc(2);
bus.write_word(font_ptr, 0x1357);
bus.write_word(sp, 0x020A); // HMGetFont selector
bus.write_long(sp + 2, font_ptr);
bus.write_word(sp + 6, 0xBEEF); // result slot poison
let result = disp.dispatch_toolbox(true, 0x030, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(font_ptr), 0);
assert_eq!(bus.read_word(sp + 6), 0);
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
#[test]
fn pack12_rgb2cmy_selector_0004_converts_each_rgb_word_to_complementary_smallfract_channel() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src_ptr = 0x350200u32;
let dst_ptr = 0x350300u32;
bus.write_word(src_ptr, 0x1357);
bus.write_word(src_ptr + 2, 0x2468);
bus.write_word(src_ptr + 4, 0x369C);
bus.write_word(dst_ptr, 0xDEAD);
bus.write_word(dst_ptr + 2, 0xBEEF);
bus.write_word(dst_ptr + 4, 0xCAFE);
bus.write_word(sp, 0x0004); // RGB2CMY selector
bus.write_long(sp + 2, dst_ptr);
bus.write_long(sp + 6, src_ptr);
let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(dst_ptr), 0xECA8);
assert_eq!(bus.read_word(dst_ptr + 2), 0xDB97);
assert_eq!(bus.read_word(dst_ptr + 4), 0xC963);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
#[test]
fn pack12_hsl2rgb_selector_0005_converts_zero_saturation_to_grayscale_rgb_words() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let src_ptr = 0x350400u32;
let dst_ptr = 0x350500u32;
bus.write_word(src_ptr, 0x1357);
bus.write_word(src_ptr + 2, 0x0000);
bus.write_word(src_ptr + 4, 0x2468);
bus.write_word(dst_ptr, 0xDEAD);
bus.write_word(dst_ptr + 2, 0xBEEF);
bus.write_word(dst_ptr + 4, 0xCAFE);
bus.write_word(sp, 0x0005); // HSL2RGB selector
bus.write_long(sp + 2, dst_ptr);
bus.write_long(sp + 6, src_ptr);
let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(dst_ptr), 0x2468);
assert_eq!(bus.read_word(dst_ptr + 2), 0x2468);
assert_eq!(bus.read_word(dst_ptr + 4), 0x2468);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
// AliasDispatch ($A823) / selector $0000 FindFolder
// IM:VI 1991 pp. 9-43..9-44: FindFolder writes foundVRefNum and foundDirID.
#[test]
fn aliasdispatch_findfolder_preferences_type_returns_found_refs() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let found_dir_id_ptr = 0x340000u32;
let found_vref_ptr = 0x340100u32;
cpu.write_reg(Register::D0, 0x0000); // FindFolder selector
bus.write_long(sp, found_dir_id_ptr);
bus.write_long(sp + 4, found_vref_ptr);
bus.write_word(sp + 8, 1); // createFolder = TRUE
bus.write_long(sp + 10, u32::from_be_bytes(*b"pref"));
bus.write_word(sp + 14, 0x8000); // kOnSystemDisk
bus.write_word(sp + 16, 0xBEEF); // result slot
let result = disp.dispatch_toolbox(true, 0x023, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 16), 0); // noErr
assert_eq!(cpu.read_reg(Register::A7), sp + 16);
assert_eq!(bus.read_word(found_vref_ptr), (-1i16) as u16);
let found_dir_id = bus.read_long(found_dir_id_ptr);
assert_ne!(found_dir_id, 0);
assert_eq!(
disp.directory_path_for_id(found_dir_id),
Some("System Folder/Preferences")
);
}
// AliasDispatch ($A823) / selector $0002 NewAlias
// IM:VI 1991 pp. 27-12..27-13: NewAlias allocates storage and writes AliasHandle.
#[test]
fn aliasdispatch_newalias_returns_allocated_alias_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let alias_out_ptr = 0x341000u32;
let target_spec_ptr = 0x341100u32;
// FSSpec target record (vRefNum, dirID, name).
bus.write_word(target_spec_ptr, (-1i16) as u16);
bus.write_long(target_spec_ptr + 2, 2);
bus.write_byte(target_spec_ptr + 6, 5);
bus.write_bytes(target_spec_ptr + 7, b"Prefs");
cpu.write_reg(Register::D0, 0x0002); // NewAlias selector
bus.write_long(sp, alias_out_ptr);
bus.write_long(sp + 4, target_spec_ptr);
bus.write_long(sp + 8, 0); // fromFile = NIL
bus.write_word(sp + 12, 0xBEEF); // result slot
let result = disp.dispatch_toolbox(true, 0x023, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0); // noErr
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
let alias_handle = bus.read_long(alias_out_ptr);
assert_ne!(alias_handle, 0);
let alias_data_ptr = bus.read_long(alias_handle);
assert_ne!(alias_data_ptr, 0);
assert_eq!(bus.read_word(alias_data_ptr), 16);
}
// AliasDispatch ($A823) / selector $000C ResolveAliasFile
// IM:VI 1991 pp. 9-30..9-31: non-alias input returns noErr and wasAliased=FALSE.
#[test]
fn aliasdispatch_resolvealiasfile_non_alias_returns_false_flags_and_preserves_spec() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let was_aliased_ptr = 0x342000u32;
let target_is_folder_ptr = 0x342100u32;
let spec_ptr = 0x342200u32;
// FSSpec input for a regular file path.
bus.write_word(spec_ptr, (-1i16) as u16);
bus.write_long(spec_ptr + 2, 2);
bus.write_byte(spec_ptr + 6, 8);
bus.write_bytes(spec_ptr + 7, b"ReadMe!!");
let spec_before = bus.read_bytes(spec_ptr, 32);
bus.write_byte(was_aliased_ptr, 0xFF);
bus.write_byte(target_is_folder_ptr, 0xFF);
cpu.write_reg(Register::D0, 0x000C); // ResolveAliasFile selector
bus.write_long(sp, was_aliased_ptr);
bus.write_long(sp + 4, target_is_folder_ptr);
bus.write_word(sp + 8, 1); // resolveAliasChains = TRUE
bus.write_long(sp + 10, spec_ptr);
bus.write_word(sp + 14, 0xBEEF); // result slot
let result = disp.dispatch_toolbox(true, 0x023, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 14), 0); // noErr
assert_eq!(cpu.read_reg(Register::A7), sp + 14);
assert_eq!(bus.read_byte(was_aliased_ptr), 0);
assert_eq!(bus.read_byte(target_is_folder_ptr), 0);
assert_eq!(bus.read_bytes(spec_ptr, 32), spec_before);
}
// Pack8 / Apple Events ($A816)
// Inside Macintosh: Interapplication Communication 1993:
// AEInstallEventHandler pp.4-62..4-64, AEProcessAppleEvent pp.4-66..4-67.
#[test]
fn pack8_unhandled_selector_pops_param_words_and_returns_noerr() {
// High byte of D0.W encodes parameter word count for Pack8 calls.
// Keep this generic selector-pop contract pinned for stubbed routines.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::D0, 0x0720); // 7 words params, routine $20
for i in 0..14 {
bus.write_byte(sp + i, 0);
}
bus.write_word(sp + 14, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 14), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 14);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
#[test]
fn pack8_aeinstalleventhandler_installs_dispatch_entry_for_event_class_and_id() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let event_class = u32::from_be_bytes(*b"aevt");
let event_id = u32::from_be_bytes(*b"oapp");
let handler_ptr = 0x00AB_CDEFu32;
let handler_refcon = 0x0102_0304u32;
// Selector $091F => AEInstallEventHandler.
cpu.write_reg(Register::D0, 0x091F);
bus.write_word(sp, 0); // isSysHandler = FALSE
bus.write_long(sp + 2, handler_refcon);
bus.write_long(sp + 6, handler_ptr);
bus.write_long(sp + 10, event_id);
bus.write_long(sp + 14, event_class);
bus.write_word(sp + 18, 0xBEEF); // OSErr result slot
let result = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 18), 0); // noErr
assert_eq!(cpu.read_reg(Register::A7), sp + 18);
assert_eq!(
disp.ae_handlers.get(&(event_class, event_id)),
Some(&(handler_ptr, handler_refcon))
);
}
#[test]
fn pack8_aeinstalleventhandler_replaces_existing_entry_for_same_event_key() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let event_class = u32::from_be_bytes(*b"aevt");
let event_id = u32::from_be_bytes(*b"oapp");
// Inside Macintosh: Interapplication Communication p.4-64:
// an existing entry for the same class/ID is replaced.
cpu.write_reg(Register::D0, 0x091F);
bus.write_word(sp, 0); // isSysHandler
bus.write_long(sp + 2, 0x1111_2222); // refcon #1
bus.write_long(sp + 6, 0x00AA_0001); // handler #1
bus.write_long(sp + 10, event_id);
bus.write_long(sp + 14, event_class);
bus.write_word(sp + 18, 0xBEEF);
let first = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(first.is_some());
assert!(first.unwrap().is_ok());
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x091F);
bus.write_word(sp, 0); // isSysHandler
bus.write_long(sp + 2, 0x3333_4444); // refcon #2
bus.write_long(sp + 6, 0x00BB_0002); // handler #2
bus.write_long(sp + 10, event_id);
bus.write_long(sp + 14, event_class);
bus.write_word(sp + 18, 0xBEEF);
let second = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(second.is_some());
assert!(second.unwrap().is_ok());
assert_eq!(
disp.ae_handlers.get(&(event_class, event_id)),
Some(&(0x00BB_0002u32, 0x3333_4444u32))
);
}
#[test]
fn pack8_aeprocessappleevent_dispatches_matching_event_to_installed_handler() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let event_class = u32::from_be_bytes(*b"aevt");
let event_id = u32::from_be_bytes(*b"oapp");
let handler_ptr = 0x0040_8000u32;
let handler_refcon = 0xDEAD_BEEFu32;
let event_record_ptr = 0x0032_0000u32;
let return_pc = 0x00F0_1234u32;
// Install OAPP handler first (selector $091F).
cpu.write_reg(Register::D0, 0x091F);
bus.write_word(sp, 0); // isSysHandler
bus.write_long(sp + 2, handler_refcon);
bus.write_long(sp + 6, handler_ptr);
bus.write_long(sp + 10, event_id);
bus.write_long(sp + 14, event_class);
bus.write_word(sp + 18, 0xBEEF);
let install = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(install.is_some());
assert!(install.unwrap().is_ok());
// Selector $021B => AEProcessAppleEvent(eventRecord).
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::PC, return_pc);
cpu.write_reg(Register::D0, 0x021B);
bus.write_long(sp, event_record_ptr);
bus.write_word(sp + 4, 0xBEEF); // OSErr result slot
let process = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(process.is_some());
assert!(process.unwrap().is_ok());
// AEProcessAppleEvent dispatches to the matching handler.
assert_eq!(cpu.read_reg(Register::PC), handler_ptr);
assert_eq!(cpu.read_reg(Register::A7), sp - 12);
assert!(disp.fired_oapp_handler);
let state = disp
.ae_call_state
.clone()
.expect("expected in-flight AE call state");
assert_eq!(state.return_pc, return_pc);
assert_eq!(state.expected_sp_after_rtd, sp + 4);
assert_eq!(
bus.read_long(sp - 12),
disp.ae_trampoline_addr
.expect("trampoline should be allocated")
);
assert_eq!(bus.read_long(sp - 8), handler_refcon);
assert_ne!(bus.read_long(sp - 4), 0, "reply AEDesc pointer");
}
#[test]
fn pack8_aeprocessappleevent_returns_handler_result_code_to_caller() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let event_class = u32::from_be_bytes(*b"aevt");
let event_id = u32::from_be_bytes(*b"oapp");
let handler_ptr = 0x0040_9000u32;
let return_pc = 0x00F0_5678u32;
let handler_result = (-1708i16) as u16; // errAEEventNotHandled
// Install handler.
cpu.write_reg(Register::D0, 0x091F);
bus.write_word(sp, 0); // isSysHandler
bus.write_long(sp + 2, 0x1111_2222);
bus.write_long(sp + 6, handler_ptr);
bus.write_long(sp + 10, event_id);
bus.write_long(sp + 14, event_class);
bus.write_word(sp + 18, 0xBEEF);
let install = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(install.is_some());
assert!(install.unwrap().is_ok());
// Enter AEProcessAppleEvent path and dispatch handler.
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::PC, return_pc);
cpu.write_reg(Register::D0, 0x021B);
bus.write_long(sp, 0x0032_1000); // EventRecord ptr
bus.write_word(sp + 4, 0xBEEF); // OSErr slot
let process = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(process.is_some());
assert!(process.unwrap().is_ok());
let state = disp
.ae_call_state
.clone()
.expect("expected in-flight AE call state");
// Simulate handler writing function result then returning through
// trampoline (`MOVE.W #$FEFE, D0; _Pack8`).
bus.write_word(state.expected_sp_after_rtd, handler_result);
cpu.write_reg(Register::A7, state.expected_sp_after_rtd);
cpu.write_reg(Register::D0, 0xFEFE);
let trampoline = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(trampoline.is_some());
assert!(trampoline.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::PC), return_pc);
assert_eq!(cpu.read_reg(Register::A7), state.expected_sp_after_rtd);
assert_eq!(bus.read_word(state.expected_sp_after_rtd), handler_result);
assert_eq!(
cpu.read_reg(Register::D0),
handler_result as i16 as i32 as u32
);
assert!(disp.ae_call_state.is_none());
}
#[test]
fn pack8_aeprocessappleevent_dispatches_matching_event_on_repeated_calls() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let event_class = u32::from_be_bytes(*b"aevt");
let event_id = u32::from_be_bytes(*b"oapp");
let handler_ptr = 0x0040_A000u32;
let handler_result = (-1708i16) as u16; // errAEEventNotHandled
let first_return_pc = 0x00F0_1234u32;
let second_return_pc = 0x00F0_5678u32;
// Install handler.
cpu.write_reg(Register::D0, 0x091F);
bus.write_word(sp, 0); // isSysHandler
bus.write_long(sp + 2, 0x1111_2222);
bus.write_long(sp + 6, handler_ptr);
bus.write_long(sp + 10, event_id);
bus.write_long(sp + 14, event_class);
bus.write_word(sp + 18, 0xBEEF);
let install = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(install.is_some());
assert!(install.unwrap().is_ok());
// First AEProcessAppleEvent dispatch.
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::PC, first_return_pc);
cpu.write_reg(Register::D0, 0x021B);
bus.write_long(sp, 0x0032_1000);
bus.write_word(sp + 4, 0xBEEF);
let process1 = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(process1.is_some());
assert!(process1.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::PC), handler_ptr);
assert!(disp.fired_oapp_handler);
let state1 = disp
.ae_call_state
.clone()
.expect("expected first in-flight AE call state");
bus.write_word(state1.expected_sp_after_rtd, handler_result);
cpu.write_reg(Register::A7, state1.expected_sp_after_rtd);
cpu.write_reg(Register::D0, 0xFEFE);
let tramp1 = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(tramp1.is_some());
assert!(tramp1.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::PC), first_return_pc);
assert_eq!(
cpu.read_reg(Register::D0),
handler_result as i16 as i32 as u32
);
assert!(disp.ae_call_state.is_none());
// Second AEProcessAppleEvent dispatch should still fire.
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::PC, second_return_pc);
cpu.write_reg(Register::D0, 0x021B);
bus.write_long(sp, 0x0032_2000);
bus.write_word(sp + 4, 0xBEEF);
let process2 = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(process2.is_some());
assert!(process2.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::PC), handler_ptr);
assert!(disp.fired_oapp_handler);
let state2 = disp
.ae_call_state
.clone()
.expect("expected second in-flight AE call state");
bus.write_word(state2.expected_sp_after_rtd, handler_result);
cpu.write_reg(Register::A7, state2.expected_sp_after_rtd);
cpu.write_reg(Register::D0, 0xFEFE);
let tramp2 = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
assert!(tramp2.is_some());
assert!(tramp2.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::PC), second_return_pc);
assert_eq!(
cpu.read_reg(Register::D0),
handler_result as i16 as i32 as u32
);
assert!(disp.ae_call_state.is_none());
}
// ScriptUtil ($A8B5) selector 0 FontScript
// IM:V 1988 pp. V-288 and V-315
#[test]
fn scriptutil_fontscript_returns_smroman_and_pops_selector_long() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0000_0000); // selector 0 (FontScript)
bus.write_word(sp + 4, 0xBEEF); // result slot
let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0); // smRoman
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// ScriptUtil ($A8B5) selector 8 GetEnvirons
// IM:V 1988 pp. V-288 and V-311
#[test]
fn scriptutil_getenvirons_returns_zero_long_and_pops_selector_plus_verb() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0000_0008); // selector 8
bus.write_word(sp + 4, 0x1234); // verb
bus.write_long(sp + 6, 0xDEAD_BEEF); // LongInt result slot
let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 6), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 6);
}
// ScriptUtil ($A8B5) selector 10 SetEnvirons
// IM:V 1988 pp. V-288 and V-311
#[test]
fn scriptutil_setenvirons_returns_noerr_and_pops_selector_verb_param() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0000_000A); // selector 10
bus.write_word(sp + 4, 0x0001); // verb
bus.write_long(sp + 6, 0xCAFE_BABE); // param
bus.write_word(sp + 10, 0xBEEF); // OSErr result slot
let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 10), 0); // noErr
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
// ScriptUtil ($A8B5) selector 12 GetScript
// IM:V 1988 pp. V-288 and V-312..V-313
#[test]
fn scriptutil_getscript_returns_zero_long_and_pops_selector_script_verb() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0000_000C); // selector 12
bus.write_word(sp + 4, 0); // smRoman
bus.write_word(sp + 6, 1); // smScriptRight
bus.write_long(sp + 8, 0xDEAD_BEEF); // LongInt result slot
let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 8), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// ScriptUtil ($A8B5) selector 14 SetScript
// IM:V 1988 pp. V-288 and V-312..V-313
#[test]
fn scriptutil_setscript_returns_noerr_and_pops_selector_script_verb_param() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0000_000E); // selector 14
bus.write_word(sp + 4, 0); // smRoman
bus.write_word(sp + 6, 1); // smScriptRight
bus.write_long(sp + 8, 0); // param
bus.write_word(sp + 12, 0xBEEF); // OSErr result slot
let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 12), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 12);
assert_eq!(cpu.read_reg(Register::D0), 0);
}
// ScriptUtil ($A8B5) selector 20 Pixel2Char
// IM:V 1988 p. V-310
// Pascal calling convention: leadingEdge VAR pointer is the LAST arg
// pushed, so it lives at sp+4 just after the selector long; textBuf
// is the FIRST arg pushed and lives deepest at sp+14.
#[test]
fn scriptutil_pixel2char_returns_offset_zero_and_clears_leadingedge() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let leading_edge_ptr = 0x362000u32;
bus.write_long(sp, 0x0000_0014); // selector 20
bus.write_long(sp + 4, leading_edge_ptr); // VAR leadingEdge (last arg)
bus.write_word(sp + 8, 12); // pixelWidth
bus.write_word(sp + 10, 0); // slop
bus.write_word(sp + 12, 8); // textLen
bus.write_long(sp + 14, 0x363000); // textBuf (first arg, deepest)
bus.write_word(sp + 18, 0xBEEF); // INTEGER result
bus.write_byte(leading_edge_ptr, 0xFF);
let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_byte(leading_edge_ptr), 0); // FALSE
assert_eq!(bus.read_word(sp + 18), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 18);
}
// ScriptUtil ($A8B5) selector 26 FindWord
// IM:V 1988 pp. V-312..V-313
// Pascal calling convention: VAR offsets pointer is the LAST arg
// pushed (sp+4); textPtr is FIRST and deepest (sp+18). OffsetTable
// is 12 bytes (ARRAY[0..2] OF OffPair) per IM:VI 33514.
#[test]
fn scriptutil_findword_zeros_offsettable_and_pops_selector_plus_args() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let offsets_ptr = 0x364000u32;
bus.write_long(sp, 0x0000_001A); // selector 26
bus.write_long(sp + 4, offsets_ptr); // VAR offsets (last arg)
bus.write_long(sp + 8, 0x366000); // breaksPtr
bus.write_word(sp + 12, 1); // leadingEdge
bus.write_word(sp + 14, 4); // offset
bus.write_word(sp + 16, 12); // textLength
bus.write_long(sp + 18, 0x365000); // textPtr (first arg, deepest)
bus.write_bytes(offsets_ptr, &[0xA5; 12]);
let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_bytes(offsets_ptr, 12), vec![0; 12]);
assert_eq!(cpu.read_reg(Register::A7), sp + 22);
}
// ScriptUtil ($A8B5) selector $820CFFDE TruncText
// IM:VI 1991 pp. 14-59..14-60 and Table C-3.
#[test]
fn scriptutil_trunctext_returns_not_truncated_and_pops_encoded_frame() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
let length_ptr = 0x366000u32;
bus.write_long(sp, 0x820C_FFDE); // TruncText encoded selector
bus.write_word(sp + 4, 0); // truncWhere
bus.write_long(sp + 6, length_ptr); // VAR length
bus.write_long(sp + 10, 0x367000); // textPtr
bus.write_word(sp + 14, 80); // width
bus.write_word(sp + 16, 0xBEEF); // INTEGER result
bus.write_word(length_ptr, 12);
let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 16), 0); // smNotTruncated
assert_eq!(bus.read_word(length_ptr), 12);
assert_eq!(cpu.read_reg(Register::A7), sp + 16);
}
// ScriptUtil ($A8B5) encoded selector fallback
// IM:VI Table C-3 stores result size and argument byte count in the high word.
#[test]
fn scriptutil_unknown_encoded_selector_uses_stack_metadata() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x8204_ABCD); // 2-byte result, 4 arg bytes
bus.write_long(sp + 4, 0xCAFE_BABE); // opaque args
bus.write_word(sp + 8, 0xBEEF); // result slot
let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 8), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// FMSwapFont ($A901)
// IM:I 1985 pp. I-223 and I-225.
#[test]
fn fmswapfont_returns_fmoutptr_and_pops_fminput_frame() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// FMInput PACKED RECORD by value (16 bytes).
bus.write_word(sp, 3); // family
bus.write_word(sp + 2, 12); // size
bus.write_byte(sp + 4, 0); // face
bus.write_byte(sp + 5, 1); // needBits
bus.write_word(sp + 6, 0); // device
bus.write_word(sp + 8, 1); // numer.v
bus.write_word(sp + 10, 1); // numer.h
bus.write_word(sp + 12, 1); // denom.v
bus.write_word(sp + 14, 1); // denom.h
bus.write_long(sp + 16, 0); // result slot
let result = disp.dispatch_toolbox(true, 0x101, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let fm_out_ptr = bus.read_long(sp + 16);
assert_ne!(fm_out_ptr, 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 16);
}
// FMSwapFont ($A901)
// IM:I 1985 p. I-225: FMOutput.errNum is 0 and the output record
// carries a non-NIL fontHandle plus scaling fields.
#[test]
fn fmswapfont_writes_non_nil_font_handle_and_fixed_point_scaling_fields() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 3); // family
bus.write_word(sp + 2, 12); // size
bus.write_byte(sp + 4, 0); // face
bus.write_byte(sp + 5, 1); // needBits
bus.write_word(sp + 6, 0); // device
bus.write_word(sp + 8, 1); // numer.v
bus.write_word(sp + 10, 1); // numer.h
bus.write_word(sp + 12, 1); // denom.v
bus.write_word(sp + 14, 1); // denom.h
bus.write_long(sp + 16, 0); // result slot
let result = disp.dispatch_toolbox(true, 0x101, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let fm_out_ptr = bus.read_long(sp + 16);
assert_ne!(fm_out_ptr, 0);
assert_eq!(bus.read_word(fm_out_ptr), 0); // errNum
assert_ne!(bus.read_long(fm_out_ptr + 2), 0); // fontHandle is non-NIL
assert_eq!(bus.read_byte(fm_out_ptr + 13), 9); // ascent for size 12
assert_eq!(bus.read_byte(fm_out_ptr + 14), 1); // descent
assert_eq!(bus.read_byte(fm_out_ptr + 15), 7); // widMax
assert_eq!(bus.read_byte(fm_out_ptr + 16), 0); // leading
assert_eq!(bus.read_word(fm_out_ptr + 18), 0x0100); // numer.v
assert_eq!(bus.read_word(fm_out_ptr + 20), 0x0100); // numer.h
assert_eq!(bus.read_word(fm_out_ptr + 22), 0x0100); // denom.v
assert_eq!(bus.read_word(fm_out_ptr + 24), 0x0100); // denom.h
}
// FMSwapFont ($A901)
// IM:I 1985 p. I-225: the output record's fontHandle is a handle to
// the chosen font record, not the FMOutput block itself.
#[test]
fn fmswapfont_writes_distinct_non_nil_font_handle() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 3); // family
bus.write_word(sp + 2, 12); // size
bus.write_byte(sp + 4, 0); // face
bus.write_byte(sp + 5, 1); // needBits
bus.write_word(sp + 6, 0); // device
bus.write_word(sp + 8, 1); // numer.v
bus.write_word(sp + 10, 1); // numer.h
bus.write_word(sp + 12, 1); // denom.v
bus.write_word(sp + 14, 1); // denom.h
bus.write_long(sp + 16, 0); // result slot
let result = disp.dispatch_toolbox(true, 0x101, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let fm_out_ptr = bus.read_long(sp + 16);
assert_ne!(fm_out_ptr, 0);
let font_handle = bus.read_long(fm_out_ptr + 2);
assert_ne!(font_handle, 0);
assert_ne!(font_handle, fm_out_ptr);
assert_eq!(bus.read_word(fm_out_ptr + 18), 0x0100); // numer.v
assert_eq!(bus.read_word(fm_out_ptr + 20), 0x0100); // numer.h
assert_eq!(bus.read_word(fm_out_ptr + 22), 0x0100); // denom.v
assert_eq!(bus.read_word(fm_out_ptr + 24), 0x0100); // denom.h
}
// FMSwapFont ($A901)
// HLE retains a compact input signature in the auxiliary word of
// the returned font-handle block for later font-manager consumers.
#[test]
fn fmswapfont_aux_font_handle_carries_input_signature() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 3); // family
bus.write_word(sp + 2, 12); // size
bus.write_byte(sp + 4, 0); // face
bus.write_byte(sp + 5, 1); // needBits
bus.write_word(sp + 6, 0); // device
bus.write_word(sp + 8, 1); // numer.v
bus.write_word(sp + 10, 1); // numer.h
bus.write_word(sp + 12, 1); // denom.v
bus.write_word(sp + 14, 1); // denom.h
bus.write_long(sp + 16, 0); // result slot
let result = disp.dispatch_toolbox(true, 0x101, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let fm_out_ptr = bus.read_long(sp + 16);
let font_handle = bus.read_long(fm_out_ptr + 2);
assert_eq!(bus.read_long(font_handle + 4), 0x0003_000C);
}
// RealFont ($A902)
// IM:I 1985 p. I-223: applFont always returns FALSE.
#[test]
fn realfont_applfont_always_returns_false_and_pops_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 12); // size
bus.write_word(sp + 2, 1); // fontNum = applFont
bus.write_word(sp + 4, 0xBEEF); // Boolean result slot
let result = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// RealFont ($A902)
// IM:I 1985 p. I-223: return TRUE if size is available; FALSE if scaling needed.
#[test]
fn realfont_known_bitmap_size_true_and_nonstandard_size_false() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 12); // size
bus.write_word(sp + 2, 3); // Geneva
bus.write_word(sp + 4, 0xBEEF);
let first = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
assert!(first.is_some());
assert!(first.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0x0100);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
cpu.write_reg(Register::A7, sp);
bus.write_word(sp, 11); // non-standard bitmap size in current HLE model
bus.write_word(sp + 2, 3);
bus.write_word(sp + 4, 0xBEEF);
let second = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
assert!(second.is_some());
assert!(second.unwrap().is_ok());
assert_eq!(bus.read_word(sp + 4), 0x0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// RealFont ($A902)
// IM:I 1985 p. I-223 line 7309: "RealFont will always return
// FALSE if you pass applFont in fontNum." Systemless HLE follows
// the Apple-canonical rule; BasiliskII System 7.5 ROM diverges
// (returns TRUE because applFont is bound to a real font at
// boot) — see a902_diag_realfont diagnostic.
#[test]
fn realfont_applfont_returns_false_for_all_canonical_sizes_per_apple_spec() {
let canonical_sizes: [u16; 6] = [9, 10, 12, 14, 18, 24];
for &size in canonical_sizes.iter() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, size);
bus.write_word(sp + 2, 1); // applFont
bus.write_word(sp + 4, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_word(sp + 4),
0,
"applFont must return FALSE at canonical size {size}",
);
}
}
// RealFont ($A902)
// IM:I 1985 p. I-223 line 7307: "FALSE if the font has to be
// scaled to that size." Systemless HLE returns FALSE for sizes
// outside the canonical bitmap set {9, 10, 12, 14, 18, 24}
// even for real bundled bitmap fonts. BasiliskII System 7.5
// ROM diverges (treats any valid fontNum as truthy regardless
// of size) — see a902_diag_realfont diagnostic.
#[test]
fn realfont_non_standard_size_returns_false_for_real_bitmap_font_per_apple_spec() {
let non_standard_sizes: [u16; 5] = [8, 11, 13, 15, 100];
for &size in non_standard_sizes.iter() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, size);
bus.write_word(sp + 2, 3); // Geneva
bus.write_word(sp + 4, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(
bus.read_word(sp + 4),
0,
"Geneva at non-canonical size {size} must return FALSE",
);
}
}
// SetFontLock ($A903)
// IM:I 1985 p. I-223: PROCEDURE SetFontLock(lockFlag: BOOLEAN).
// Pops one 2-byte BOOLEAN argument; no function-result slot.
#[test]
fn setfontlock_true_pops_two_byte_boolean_argument_frame() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0100); // TRUE in high byte
bus.write_word(sp + 2, 0xCAFE); // sentinel above pop window
bus.write_word(sp + 4, 0xF00D); // additional sentinel
let result = disp.dispatch_toolbox(true, 0x103, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
// Sentinels above the pop window survive — trap doesn't write
// past the 2-byte argument slot.
assert_eq!(bus.read_word(sp + 2), 0xCAFE);
assert_eq!(bus.read_word(sp + 4), 0xF00D);
}
#[test]
fn setfontlock_false_pops_two_byte_boolean_argument_frame() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_word(sp, 0x0000); // FALSE
bus.write_word(sp + 2, 0xBABE); // sentinel above pop window
bus.write_word(sp + 4, 0xBEEF); // additional sentinel
let result = disp.dispatch_toolbox(true, 0x103, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
// FALSE branch pops the same 2 bytes as TRUE (Pascal PROCEDURE
// calling convention is value-independent).
assert_eq!(bus.read_word(sp + 2), 0xBABE);
assert_eq!(bus.read_word(sp + 4), 0xBEEF);
}
#[test]
fn setfontlock_alternating_calls_have_net_sp_delta_zero() {
// Eight alternating TRUE/FALSE calls — each pops exactly 2
// bytes; net SP delta after the last pop equals starting SP
// minus the cumulative argument bytes (verifies no value-
// dependent per-call drift).
let (mut disp, mut cpu, mut bus) = setup();
let base = TEST_SP;
let mut sp = base;
for i in 0..8u32 {
let value: u16 = if i & 1 == 0 { 0x0100 } else { 0x0000 };
bus.write_word(sp, value);
cpu.write_reg(Register::A7, sp);
let result = disp.dispatch_toolbox(true, 0x103, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
sp = cpu.read_reg(Register::A7);
}
assert_eq!(sp - base, 16);
}
// Fix2Frac ($A841)
// Operating System Utilities 1994, p. 3-44; IM IV 1986, p. IV-65.
#[test]
fn fix2frac_returns_equivalent_fract_and_saturates_out_of_range_inputs() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// IM IV-65 example: Fix2Frac(X2Fix(1.75)) = $70000000.
bus.write_long(sp, 0x0001_C000);
bus.write_long(sp + 4, 0);
let first = disp.dispatch_toolbox(true, 0x041, &mut cpu, &mut bus);
assert!(first.is_some());
assert!(first.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x7000_0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
// OS Utils 3-44: values above Fract max saturate to $7FFFFFFF.
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x0002_0000); // +2.0 Fixed
bus.write_long(sp + 4, 0);
let high = disp.dispatch_toolbox(true, 0x041, &mut cpu, &mut bus);
assert!(high.is_some());
assert!(high.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x7FFF_FFFF);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
// OS Utils 3-44: values below Fract min saturate to $80000000.
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, (-0x0002_0000i32) as u32); // -2.0 Fixed
bus.write_long(sp + 4, 0);
let low = disp.dispatch_toolbox(true, 0x041, &mut cpu, &mut bus);
assert!(low.is_some());
assert!(low.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x8000_0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// Frac2Fix ($A842)
// Operating System Utilities 1994, p. 3-44; IM IV 1986, p. IV-65.
#[test]
fn frac2fix_matches_documented_positive_and_negative_examples() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// IM IV-65 example: Frac2Fix(X2Frac(1.75)) = $0001C000.
bus.write_long(sp, 0x7000_0000);
bus.write_long(sp + 4, 0);
let pos = disp.dispatch_toolbox(true, 0x042, &mut cpu, &mut bus);
assert!(pos.is_some());
assert!(pos.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x0001_C000);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
// IM IV-65 example: Frac2Fix(X2Frac(-1.75)) = $FFFE4000.
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x9000_0000);
bus.write_long(sp + 4, 0);
let neg = disp.dispatch_toolbox(true, 0x042, &mut cpu, &mut bus);
assert!(neg.is_some());
assert!(neg.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0xFFFE_4000);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// Fix2X ($A843)
// Operating System Utilities 1994, p. 3-45.
#[test]
fn fix2x_returns_extended_equivalent_and_pops_fixed_argument() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0001_C000); // 1.75 Fixed
for i in 0..10 {
bus.write_byte(sp + 4 + i, 0);
}
let result = disp.dispatch_toolbox(true, 0x043, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let ext = Extended80::read_from_bus(&bus, sp + 4);
assert!((f64::from(ext) - 1.75).abs() < 1e-12);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// X2Fix ($A844)
// Operating System Utilities 1994, p. 3-45.
#[test]
fn x2fix_returns_best_fixed_approximation_and_saturates_out_of_range() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
Extended80::from(1.75).write_to_bus(&mut bus, sp);
bus.write_long(sp + 10, 0);
let exact = disp.dispatch_toolbox(true, 0x044, &mut cpu, &mut bus);
assert!(exact.is_some());
assert!(exact.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 10), 0x0001_C000);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
cpu.write_reg(Register::A7, sp);
Extended80::from(40000.0).write_to_bus(&mut bus, sp);
bus.write_long(sp + 10, 0);
let high = disp.dispatch_toolbox(true, 0x044, &mut cpu, &mut bus);
assert!(high.is_some());
assert!(high.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 10), 0x7FFF_FFFF);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
cpu.write_reg(Register::A7, sp);
Extended80::from(-40000.0).write_to_bus(&mut bus, sp);
bus.write_long(sp + 10, 0);
let low = disp.dispatch_toolbox(true, 0x044, &mut cpu, &mut bus);
assert!(low.is_some());
assert!(low.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 10), 0x8000_0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
// Frac2X ($A845)
// Operating System Utilities 1994, p. 3-46.
#[test]
fn frac2x_returns_extended_equivalent_and_pops_fract_argument() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x7000_0000); // 1.75 Fract
for i in 0..10 {
bus.write_byte(sp + 4 + i, 0);
}
let result = disp.dispatch_toolbox(true, 0x045, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
let ext = Extended80::read_from_bus(&bus, sp + 4);
assert!((f64::from(ext) - 1.75).abs() < 1e-12);
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
}
// X2Frac ($A846)
// Operating System Utilities 1994, p. 3-46.
#[test]
fn x2frac_returns_best_fract_approximation_and_saturates_out_of_range() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
Extended80::from(1.75).write_to_bus(&mut bus, sp);
bus.write_long(sp + 10, 0);
let exact = disp.dispatch_toolbox(true, 0x046, &mut cpu, &mut bus);
assert!(exact.is_some());
assert!(exact.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 10), 0x7000_0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
cpu.write_reg(Register::A7, sp);
Extended80::from(3.0).write_to_bus(&mut bus, sp);
bus.write_long(sp + 10, 0);
let high = disp.dispatch_toolbox(true, 0x046, &mut cpu, &mut bus);
assert!(high.is_some());
assert!(high.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 10), 0x7FFF_FFFF);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
cpu.write_reg(Register::A7, sp);
Extended80::from(-3.0).write_to_bus(&mut bus, sp);
bus.write_long(sp + 10, 0);
let low = disp.dispatch_toolbox(true, 0x046, &mut cpu, &mut bus);
assert!(low.is_some());
assert!(low.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 10), 0x8000_0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 10);
}
// FracCos ($A847)
// Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-42.
#[test]
fn fraccos_zero_radians_returns_plus_one_fract() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0000_0000); // 0.0 radians in Fixed
bus.write_long(sp + 4, 0);
let result = disp.dispatch_toolbox(true, 0x047, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x4000_0000);
}
#[test]
fn fraccos_consumes_fixed_argument_and_writes_result_slot() {
// FUNCTION FracCos(x: Fixed): Fract.
// One 4-byte Fixed argument consumed; 4-byte Fract result at post-pop [SP].
// Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-42.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0001_0000); // 1.0 radians in Fixed
bus.write_long(sp + 4, 0xDEAD_BEEF);
let result = disp.dispatch_toolbox(true, 0x047, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_ne!(
bus.read_long(sp + 4),
0xDEAD_BEEF,
"FracCos should write a Fract result to the function-result slot"
);
}
// FracSin ($A848)
// Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-42.
#[test]
fn fracsin_zero_radians_returns_zero_fract() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0000_0000); // 0.0 radians in Fixed
bus.write_long(sp + 4, 0xDEAD_BEEF);
let result = disp.dispatch_toolbox(true, 0x048, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x0000_0000);
}
#[test]
fn fracsin_consumes_fixed_argument_and_writes_result_slot() {
// FUNCTION FracSin(x: Fixed): Fract.
// One 4-byte Fixed argument consumed; 4-byte Fract result at post-pop [SP].
// Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-42.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x0001_0000); // 1.0 radians in Fixed
bus.write_long(sp + 4, 0xDEAD_BEEF);
let result = disp.dispatch_toolbox(true, 0x048, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_ne!(
bus.read_long(sp + 4),
0xDEAD_BEEF,
"FracSin should write a Fract result to the function-result slot"
);
}
// FracSqrt ($A849)
// Inside Macintosh IV (1986), pp. IV-64..IV-65; OS Utils (1994), p. 3-41.
#[test]
fn fracsqrt_matches_documented_iv65_example_value() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// IM:IV IV-65 example: FracSqrt(X2Frac(1.96)) = $5999999A.
// X2Frac(1.96) = $7D70A3D7.
bus.write_long(sp, 0x7D70_A3D7);
bus.write_long(sp + 4, 0);
let result = disp.dispatch_toolbox(true, 0x049, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x5999_999A);
}
#[test]
fn fracsqrt_interprets_input_as_unsigned_fract() {
// IM:IV IV-64 and OS Utils 3-41: FracSqrt interprets x as unsigned
// 0..4-2^-30, so bit 31 carries weight +2 instead of -2.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// $C0000000 is -1.0 as signed Fract but 3.0 in unsigned Fract domain.
// sqrt(3.0) in Fract rounds to $6ED9EBA1.
bus.write_long(sp, 0xC000_0000);
bus.write_long(sp + 4, 0);
let result = disp.dispatch_toolbox(true, 0x049, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 4), 0x6ED9_EBA1);
}
#[test]
fn fracsqrt_consumes_fract_argument_and_writes_result_slot() {
// FUNCTION FracSqrt(x: Fract): Fract.
// One 4-byte Fract argument consumed; 4-byte Fract result at post-pop [SP].
// Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-41.
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0x4000_0000); // 1.0 Fract
bus.write_long(sp + 4, 0xDEAD_BEEF);
let result = disp.dispatch_toolbox(true, 0x049, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::A7), sp + 4);
assert_ne!(
bus.read_long(sp + 4),
0xDEAD_BEEF,
"FracSqrt should write a Fract result to the function-result slot"
);
}
// FracMul ($A84A)
// Operating System Utilities 1994, p. 3-40; IM IV 1986, p. IV-65.
#[test]
fn fracmul_matches_documented_examples_and_writes_result_slot() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// IM IV-65: FracMul(X2Frac(1.50), X2Frac(1.30)) = $7CCCCCCD.
// Stack at entry: SP+0=b, SP+4=a, SP+8=result slot.
bus.write_long(sp, 0x5333_3333); // X2Frac(1.30)
bus.write_long(sp + 4, 0x6000_0000); // X2Frac(1.50)
bus.write_long(sp + 8, 0);
let pos = disp.dispatch_toolbox(true, 0x04A, &mut cpu, &mut bus);
assert!(pos.is_some());
assert!(pos.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 8), 0x7CCC_CCCD);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
// IM IV-65: FracMul(X2Frac(-1.50), X2Frac(1.30)) = $83333333.
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x5333_3333); // X2Frac(1.30)
bus.write_long(sp + 4, 0xA000_0000); // X2Frac(-1.50)
bus.write_long(sp + 8, 0);
let neg = disp.dispatch_toolbox(true, 0x04A, &mut cpu, &mut bus);
assert!(neg.is_some());
assert!(neg.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 8), 0x8333_3333);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// FracDiv ($A84B)
// Operating System Utilities 1994, pp. 3-40 to 3-41; IM IV 1986, p. IV-65.
#[test]
fn fracdiv_matches_documented_examples_and_writes_result_slot() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// IM IV-65: FracDiv(X2Frac(1.95), X2Frac(1.30)) = $60000000.
// Stack at entry: SP+0=b (denominator), SP+4=a (numerator), SP+8=result slot.
bus.write_long(sp, 0x5333_3333); // X2Frac(1.30)
bus.write_long(sp + 4, 0x7CCC_CCCD); // X2Frac(1.95)
bus.write_long(sp + 8, 0);
let pos = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
assert!(pos.is_some());
assert!(pos.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 8), 0x6000_0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
// IM IV-65: FracDiv(X2Frac(-1.95), X2Frac(1.30)) = $A0000000.
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x5333_3333); // X2Frac(1.30)
bus.write_long(sp + 4, 0x8333_3333); // X2Frac(-1.95)
bus.write_long(sp + 8, 0);
let neg = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
assert!(neg.is_some());
assert!(neg.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 8), 0xA000_0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// FracDiv ($A84B) divide-by-zero saturation.
// Operating System Utilities 1994, p. 3-41: when b==0, return
// $80000000 if a is negative, else $7FFFFFFF (including 0/0).
#[test]
fn fracdiv_divide_by_zero_saturates_with_dividend_sign() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
bus.write_long(sp, 0); // b = 0
bus.write_long(sp + 4, 0x4000_0000); // a = +1.0
bus.write_long(sp + 8, 0);
let pos = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
assert!(pos.is_some());
assert!(pos.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 8), 0x7FFF_FFFF);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0); // b = 0
bus.write_long(sp + 4, 0xC000_0000); // a = -1.0
bus.write_long(sp + 8, 0);
let neg = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
assert!(neg.is_some());
assert!(neg.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 8), 0x8000_0000);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0); // b = 0
bus.write_long(sp + 4, 0); // a = 0
bus.write_long(sp + 8, 0);
let zero = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
assert!(zero.is_some());
assert!(zero.unwrap().is_ok());
assert_eq!(bus.read_long(sp + 8), 0x7FFF_FFFF);
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// CursorDeviceDispatch ($AADB) — selector in D0, no stack args
#[test]
fn test_extended_dispatch() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x1234_5678);
let result = disp.dispatch_toolbox(true, 0x2DB, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(bus.read_word(sp + 2), 0);
}
// Movie Toolbox Dispatch ($AAAA)
#[test]
fn movietoolboxdispatch_selector_in_d0_returns_noerr_and_preserves_stack() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
cpu.write_reg(Register::A7, sp);
cpu.write_reg(Register::D0, 0x0000_0001);
cpu.write_reg(Register::D1, 0x1111_2222);
cpu.write_reg(Register::A0, 0x3333_4444);
cpu.write_reg(Register::A1, 0x5555_6666);
bus.write_word(sp, 0xCAFE);
bus.write_word(sp + 2, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
assert!(result.is_some(), "MovieToolboxDispatch should be handled");
assert!(
result.unwrap().is_ok(),
"MovieToolboxDispatch should return"
);
assert_eq!(cpu.read_reg(Register::D0), 0);
assert_eq!(cpu.read_reg(Register::D1), 0x1111_2222);
assert_eq!(cpu.read_reg(Register::A0), 0x3333_4444);
assert_eq!(cpu.read_reg(Register::A1), 0x5555_6666);
assert_eq!(cpu.read_reg(Register::A7), sp);
assert_eq!(bus.read_word(sp), 0);
assert_eq!(bus.read_word(sp + 2), 0xBEEF);
}
// Unhandled trap returns None
#[test]
fn test_unhandled_trap_returns_none() {
let (mut disp, mut cpu, mut bus) = setup();
let result = disp.dispatch_toolbox(true, 0xFFF, &mut cpu, &mut bus);
assert!(result.is_none(), "Unhandled trap should return None");
}
// FixATan2 ($A818)
// FUNCTION FixATan2(x, y: LongInt): Fixed;
// Inside Macintosh Volume IV (1986), p. IV-65.
//
// Witnesses the IM:IV IV-65 documented value bit-exactly against the
// Systemless HLE: FixATan2(X2Fix(1.0), X2Fix(1.0)) = 0x0000C910. Pascal
// LTR push: x first (lands at SP+4), y last (lands at SP+0). Trap pops
// 8 arg bytes and writes the 4-byte Fixed result into the slot at
// former SP+8.
#[test]
fn fixatan2_returns_im_documented_pi_over_four_for_one_one() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// Pascal LTR push: x first, y last → y at SP+0, x at SP+4.
bus.write_long(sp, 0x0001_0000); // y = X2Fix(1.0)
bus.write_long(sp + 4, 0x0001_0000); // x = X2Fix(1.0)
bus.write_long(sp + 8, 0xDEAD_BEEF); // result-slot poison
let result = disp.dispatch_toolbox(true, 0x018, &mut cpu, &mut bus);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
// IM:IV IV-65: FixATan2(X2Fix(1.00), X2Fix(1.00)) = $0000C910
assert_eq!(bus.read_long(sp + 8), 0x0000_C910);
// 8 arg bytes consumed.
assert_eq!(cpu.read_reg(Register::A7), sp + 8);
}
// FixATan2 ($A818): scale invariance — only y/x ratio matters.
// Per IM:IV IV-65 "arctan(type/type) -> Fixed" note.
#[test]
fn fixatan2_only_ratio_matters_scale_invariance() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// First call: raw LONGINT 1:1 ratio (y=2, x=2).
bus.write_long(sp, 2);
bus.write_long(sp + 4, 2);
let _ = disp
.dispatch_toolbox(true, 0x018, &mut cpu, &mut bus)
.unwrap();
let raw_result = bus.read_long(sp + 8);
// Second call: Fixed 1:1 ratio (y=X2Fix(1), x=X2Fix(1)).
cpu.write_reg(Register::A7, sp);
bus.write_long(sp, 0x0001_0000);
bus.write_long(sp + 4, 0x0001_0000);
let _ = disp
.dispatch_toolbox(true, 0x018, &mut cpu, &mut bus)
.unwrap();
let fixed_result = bus.read_long(sp + 8);
// Same ratio → same Fixed result; both equal IM:IV IV-65 documented value.
assert_eq!(raw_result, fixed_result);
assert_eq!(raw_result, 0x0000_C910);
}
// SysError ($A9C9) must halt the runner so the halt PC reports the
// originating SysError call site instead of letting the game execute
// past it into invalid territory.
// PROCEDURE SysError(errorCode: INTEGER);
// Inside Macintosh Volume II (1985), pp. II-358 to II-359;
// Inside Macintosh: Operating System Utilities (1994), pp. 2-13 to 2-14.
#[test]
fn test_syserror_writes_ds_err_code_and_halts_runner() {
let (mut disp, mut cpu, mut bus) = setup();
let sp = TEST_SP;
// Push errorCode (INTEGER, 16-bit) at SP.
bus.write_word(sp, 0x002A); // dsCoreErr-style code; value irrelevant
bus.write_word(crate::memory::globals::addr::DS_ERR_CODE, 0xBEEF);
let result = disp.dispatch_toolbox(true, 0x1C9, &mut cpu, &mut bus);
// Some(Err(Halted)) — handler matched AND signalled halt.
let inner = result.expect("SysError must be a handled trap");
assert!(
matches!(inner, Err(crate::Error::Halted)),
"SysError must return Err(Halted), got {:?}",
inner
);
// Stack-discipline: errorCode (2 bytes) consumed, A7 advanced.
assert_eq!(cpu.read_reg(Register::A7), sp + 2);
assert_eq!(
bus.read_word(crate::memory::globals::addr::DS_ERR_CODE),
0x002A
);
}
#[test]
fn initresources_returns_minus_one_for_nominal_call() {
let (mut disp, mut cpu, mut bus) = setup();
cpu.write_reg(Register::A7, TEST_SP);
let init = disp.dispatch_toolbox(true, 0x195, &mut cpu, &mut bus);
assert!(init.is_some(), "InitResources should be handled");
assert!(
init.unwrap().is_ok(),
"InitResources should return normally"
);
assert_eq!(bus.read_word(TEST_SP) as i16, -1);
assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
}
#[test]
fn rsrczoneinit_preserves_stack_pointer() {
let (mut disp, mut cpu, mut bus) = setup();
let sp_before = cpu.read_reg(Register::A7);
let result = disp.dispatch_toolbox(true, 0x196, &mut cpu, &mut bus);
assert!(result.is_some(), "RsrcZoneInit should be handled");
assert!(
result.unwrap().is_ok(),
"RsrcZoneInit should return normally"
);
assert_eq!(cpu.read_reg(Register::A7), sp_before);
}
}