diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c56fc7..cb4cffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this crate are documented here. +## 0.2.1 + +- Added `StackMap::slot_owner`: the instruction whose push made a stack slot live before an instruction. A body pops a temporary and pushes another at the same height, often of another type, so a slot's offset names different variables at different points; the push that made it is the variable. Two paths that disagree about the push leave the slot without an owner rather than a guessed one. On `code-tool6_4_3.exe` every local a body names has one. +- Added `Machine` (`X86`, `X64`) and the runtime's memory layout: `Machine::size` is a type's `RealSize` and `Machine::field_offset` a record field's offset, as `TPSTypeRec.CalcSize` and `LoadTypes` compute them - records packed, static arrays their elements back to back, dynamic arrays, strings and classes a pointer, pointers and method pointers three. A type table that nests past any the compiler writes, or refers to itself, has no size rather than no end. +- Added `Try` and `ProcDisasm::tries`, `try_of` and `handler_exit`: each `try` a procedure installs, with its body and sections, and where control continues after a `PopExceptionHandler` on the path no exception took - the end of the body runs the finally section, else the second finally, else leaves; the end of the finally or except section runs the second finally, else leaves (`cm_poexh`). Read as falling through, the end of a `try..except` body ran straight into the except section. +- Fixed `StackMap` following a `PopExceptionHandler` into the next section; it follows `handler_exit`. +- Added `runtime::Builtin`: what each function the runtime registers itself (`TPSExec.RegisterStandardProcs`) does beyond computing its result - nothing, raise, update a variable passed by reference, read the exception being handled, or raise and never return. `Builtin::of` finds one by name in any case, as the runtime does; a host's function or a DLL import is not one. + ## 0.2.0 This release changes the public API: `ProcDisasm::branch_target()` / `branch_targets()` are removed, `FlagGoto`'s field is renamed, and `SetStackPointerToCopy` now carries two operands. Hence the minor version bump. diff --git a/Cargo.toml b/Cargo.toml index 5ec86e7..2596193 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pascalscript" -version = "0.2.0" +version = "0.2.1" edition = "2024" rust-version = "1.85" description = "Read-only parser + disassembler for the RemObjects PascalScript III binary container format (IFPS)" diff --git a/src/bytecode.rs b/src/bytecode.rs index 0fd5dd6..82c0a32 100644 --- a/src/bytecode.rs +++ b/src/bytecode.rs @@ -11,7 +11,7 @@ use crate::{ error::Error, header::INVALID_VAL, - opcode::{Opcode, parse_opcode}, + opcode::{ExceptionHandlerEnd, Opcode, parse_opcode}, reader::Reader, ty::Type, }; @@ -125,6 +125,99 @@ pub struct ProcDisasm<'a> { pub instructions: Vec>, } +/// One `try` a procedure installs ([`Opcode::PushExceptionHandler`]): where +/// its body and each of its sections begin, in IFPS-blob coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Try { + /// Index of the instruction that installs it. + pub at: usize, + /// The first instruction of the protected body: the one after `at`. + pub body: u32, + /// The finally section, when the `try` has one. + pub finally: Option, + /// The except section, when the `try` has one. + pub except: Option, + /// The second finally section, when the `try` has one. + pub finally2: Option, + /// Where control continues once the `try` is done. + pub end: Option, +} + +impl Try { + /// Where control continues, on the path no exception took, when a section + /// of this `try` ends ([`Opcode::PopExceptionHandler`] at `position`). + /// + /// The runtime's rule (`cm_poexh`): the end of the body runs the finally + /// section, else the second finally, else leaves; the end of the finally + /// or of the except section runs the second finally, else leaves; the end + /// of the second finally leaves. A section ended with an exception pending + /// re-raises instead, which is the exception's path, not this one. + #[must_use] + pub fn exit(&self, position: ExceptionHandlerEnd) -> Option { + match position { + ExceptionHandlerEnd::EndOfBlock => self.finally.or(self.finally2).or(self.end), + ExceptionHandlerEnd::EndOfFirstFinally | ExceptionHandlerEnd::EndOfExcept => { + self.finally2.or(self.end) + } + ExceptionHandlerEnd::EndOfSecondFinally => self.end, + } + } + + /// Whether `offset` lies within the `try` - its body or any section. + fn contains(&self, offset: u32) -> bool { + self.end + .is_some_and(|end| (self.body..end).contains(&offset)) + } +} + +impl ProcDisasm<'_> { + /// Every `try` the procedure installs, in the order they appear. + #[must_use] + pub fn tries(&self) -> Vec { + self.instructions + .iter() + .enumerate() + .filter(|(_, instruction)| { + matches!(instruction.opcode, Opcode::PushExceptionHandler { .. }) + }) + .map(|(at, instruction)| { + let [finally, except, finally2, end] = instruction.branch_targets(); + Try { + at, + body: instruction.next_offset, + finally, + except, + finally2, + end, + } + }) + .collect() + } + + /// The `try` whose section the [`Opcode::PopExceptionHandler`] at `index` + /// ends: the innermost one containing it, which is the one the runtime's + /// handler stack has on top there - the compiler nests them. + #[must_use] + pub fn try_of(&self, index: usize) -> Option { + let offset = self.instructions.get(index)?.offset; + self.tries() + .into_iter() + .filter(|candidate| candidate.contains(offset)) + .max_by_key(|candidate| candidate.at) + } + + /// Where control continues, on the path no exception took, after the + /// [`Opcode::PopExceptionHandler`] at `index` ([`Try::exit`]); `None` for + /// any other instruction, or one no `try` contains. + #[must_use] + pub fn handler_exit(&self, index: usize) -> Option { + let Opcode::PopExceptionHandler { position } = self.instructions.get(index)?.opcode else { + return None; + }; + self.try_of(index)?.exit(position) + } +} + /// Decodes the bytecode body for an internal proc. /// /// `blob` is the entire IFPS byte buffer; `bytecode_offset` and diff --git a/src/frame.rs b/src/frame.rs index 7acb99e..9085a1b 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -23,12 +23,17 @@ use crate::{ opcode::Opcode, }; -/// The stack height before each instruction of one procedure. +/// The stack height before each instruction of one procedure, and which +/// instruction pushed each slot then live. #[derive(Clone, Debug, PartialEq, Eq)] pub struct StackMap { /// Height before instruction `i` (the number of the body's own slots /// live), or `None` for an instruction no path reaches. heights: Vec>, + /// For each instruction, the instruction that pushed each live slot, `s+1` + /// first: `None` where two paths reaching it disagree about which push + /// made a slot. Empty for an instruction no path reaches. + owners: Vec>>, } /// The stack slots one call consumes: the top `count` of the caller's. @@ -67,48 +72,78 @@ impl StackMap { let instructions = &disasm.instructions; let index_of = |offset: u32| instructions.iter().position(|i| i.offset == offset); let mut heights: Vec> = vec![None; instructions.len()]; - let mut work = VecDeque::new(); + let mut owners: Vec>> = vec![Vec::new(); instructions.len()]; + let mut work: VecDeque<(usize, Vec>)> = VecDeque::new(); if !instructions.is_empty() { - work.push_back((0usize, 0u32)); + work.push_back((0usize, Vec::new())); } - while let Some((index, height)) = work.pop_front() { - let Some(slot) = heights.get_mut(index) else { + while let Some((index, arriving)) = work.pop_front() { + let height = + u32::try_from(arriving.len()).map_err(|_| imbalance(instructions.get(index)))?; + let (Some(slot), Some(known)) = (heights.get_mut(index), owners.get_mut(index)) else { continue; }; match *slot { - Some(known) if known == height => continue, - Some(_) => { + Some(recorded) if recorded != height => { return Err(imbalance(instructions.get(index))); } - None => *slot = Some(height), + Some(_) => { + // Another path: a slot two pushes may have made is owned + // by neither, and only a change is worth propagating. + let mut changed = false; + for (owner, other) in known.iter_mut().zip(&arriving) { + if owner.is_some() && owner != other { + *owner = None; + changed = true; + } + } + if !changed { + continue; + } + } + None => { + *slot = Some(height); + *known = arriving; + } } let Some(instruction) = instructions.get(index) else { continue; }; - let after = height_after(instruction, height)?; + let before = owners.get(index).cloned().unwrap_or_default(); + let after = owners_after(instruction, index, &before)?; let fallthrough = index .checked_add(1) .filter(|next| *next < instructions.len()); - let mut go = |target: Option, height: u32| { + let mut go = |target: Option, stack: &Vec>| { if let Some(target) = target.and_then(index_of) { - work.push_back((target, height)); + work.push_back((target, stack.clone())); } }; match &instruction.opcode { Opcode::Return => {} Opcode::Goto { .. } | Opcode::PopAndGoto { .. } | Opcode::Pop2AndGoto { .. } => { - go(instruction.branch_target(), after); + go(instruction.branch_target(), &after); } Opcode::CondGoto { .. } | Opcode::CondNotGoto { .. } | Opcode::FlagGoto { .. } => { - go(instruction.branch_target(), after); + go(instruction.branch_target(), &after); if let Some(next) = fallthrough { work.push_back((next, after)); } } + // The end of a `try` section continues where the runtime sends + // it, not into the next section. + Opcode::PopExceptionHandler { .. } => match disasm.handler_exit(index) { + Some(exit) => go(Some(exit), &after), + None => { + if let Some(next) = fallthrough { + work.push_back((next, after)); + } + } + }, Opcode::PushExceptionHandler { .. } => { // Each section starts at the height the `try` saved. for target in instruction.branch_targets() { - go(target, height); + go(target, &before); } if let Some(next) = fallthrough { work.push_back((next, after)); @@ -121,7 +156,22 @@ impl StackMap { } } } - Ok(Self { heights }) + Ok(Self { heights, owners }) + } + + /// The instruction that pushed stack slot `s+offset` (`offset` from 1), + /// live before instruction `index`. + /// + /// A slot's offset names a different variable at different points: a body + /// pops a temporary and pushes another at the same height, of another + /// type. The push that made it is the variable's identity. `None` when + /// the slot is not live there, the instruction is unreachable, or two paths + /// reaching it disagree about which push made the slot - which the compiler + /// never emits, and which is not guessed at. + #[must_use] + pub fn slot_owner(&self, index: usize, offset: u32) -> Option { + let position = usize::try_from(offset).ok()?.checked_sub(1)?; + self.owners.get(index)?.get(position).copied().flatten() } /// The height before instruction `index`, or `None` when no path reaches @@ -173,20 +223,29 @@ impl StackMap { } } -/// The height after `instruction` runs at `height`. -fn height_after(instruction: &Instruction<'_>, height: u32) -> Result { - let pop = |n: u32| { - height +/// The live slots' owners after `instruction`, the one at `index`, runs with +/// `before` live: a push adds a slot it owns, a pop removes the top. +fn owners_after( + instruction: &Instruction<'_>, + index: usize, + before: &[Option], +) -> Result>, Error> { + let pop = |n: usize| { + let kept = before + .len() .checked_sub(n) - .ok_or_else(|| imbalance(Some(instruction))) + .ok_or_else(|| imbalance(Some(instruction)))?; + Ok(before.get(..kept).map(<[_]>::to_vec).unwrap_or_default()) }; match instruction.opcode { - Opcode::PushType { .. } | Opcode::Push { .. } | Opcode::PushVar { .. } => height - .checked_add(1) - .ok_or_else(|| imbalance(Some(instruction))), + Opcode::PushType { .. } | Opcode::Push { .. } | Opcode::PushVar { .. } => { + let mut after = before.to_vec(); + after.push(Some(index)); + Ok(after) + } Opcode::Pop | Opcode::PopAndGoto { .. } => pop(1), Opcode::Pop2AndGoto { .. } => pop(2), - _ => Ok(height), + _ => Ok(before.to_vec()), } } @@ -308,4 +367,34 @@ mod tests { Err(Error::StackImbalance { offset: 0x100 }) ); } + + /// **A slot's identity is the push that made it.** `s+1` holds a Boolean + /// temporary, is popped, and holds an integer one: two variables at one + /// offset. A push before a branch owns its slot on both arms and where + /// they meet again. + #[test] + fn a_slot_is_owned_by_the_push_that_made_it() { + let disasm = body(vec![ + Opcode::PushType { type_no: 28 }, // 0: s+1, a Boolean + Opcode::Pop, // 1 + Opcode::PushType { type_no: 11 }, // 2: s+1, an integer + Opcode::CondGoto { + offset: 1, // to 0x105, over the nop + cond: slot(1), + }, + Opcode::Nop, // 4 + Opcode::Pop, // 5 + Opcode::Return, // 6 + ]); + let map = disasm.stack_map().unwrap(); + assert_eq!(map.slot_owner(1, 1), Some(0), "the Boolean's"); + assert_eq!( + map.slot_owner(3, 1), + Some(2), + "the integer's, at one offset" + ); + assert_eq!(map.slot_owner(5, 1), Some(2), "and where the arms meet"); + assert_eq!(map.slot_owner(6, 1), None, "popped"); + assert_eq!(map.slot_owner(3, 2), None, "never pushed"); + } } diff --git a/src/lib.rs b/src/lib.rs index 249b184..39c8ff4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,12 +71,13 @@ pub mod literal; pub mod opcode; pub mod operand; pub mod proc; +pub mod runtime; pub mod signature; pub mod ty; pub mod var; pub use attribute::{Attribute, AttributeField, AttributeFieldValue}; -pub use bytecode::{Instruction, ProcDisasm}; +pub use bytecode::{Instruction, ProcDisasm, Try}; pub use container::Container; pub use disasm::{ContainerSummary, DisasmDisplay}; pub use error::Error; @@ -86,6 +87,7 @@ pub use literal::Literal; pub use opcode::{CalcOp, CompareOp, ExceptionHandlerEnd, FlowType, Opcode}; pub use operand::{Operand, VarRef}; pub use proc::{ExternalProc, InternalProc, Proc, ProcKind}; +pub use runtime::Builtin; pub use signature::{DllImport, ExternalDecl, FrameSlot, Param, Signature}; -pub use ty::{BaseType, Type, TypeBody}; +pub use ty::{BaseType, Machine, Type, TypeBody}; pub use var::Var; diff --git a/src/runtime.rs b/src/runtime.rs new file mode 100644 index 0000000..832e75f --- /dev/null +++ b/src/runtime.rs @@ -0,0 +1,135 @@ +//! The functions the PascalScript runtime registers itself, and what each +//! does beyond computing its result. +//! +//! A script calls these like any external procedure, by name, and an +//! embedding host adds its own beside them. The runtime's own are listed in +//! `TPSExec.RegisterStandardProcs` (`uPSRuntime.pas:9905-9975`); what each does +//! is its handler's (`DefProc`, `Length_`, `SetLength_`, `GetArrayLength`, +//! `SetArrayLength`, ...). A name here is matched as the runtime matches it, +//! without regard to case: the compiler writes it upper-cased. +//! +//! Which argument a call passes by reference is visible where it is called - +//! the caller pushes a pointer for it - so what is stated here is only what the +//! declaration cannot say: whether the function writes through those +//! references, whether it can raise, and whether it reads the exception being +//! handled. + +/// What a runtime function does beyond computing its result. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum Builtin { + /// Computes its result from its arguments and nothing else. + Pure, + /// Like [`Self::Pure`], but may raise: a conversion of text that is not a + /// number, an overflowing rounding. + MayRaise, + /// Updates a variable passed by reference (`Delete`, `Insert`, `Inc`, + /// `SetLength`, `SetArrayLength`, ...). + UpdatesReference, + /// Reads the exception being handled (`ExceptionType`, `ExceptionParam`, + /// ...). + ReadsException, + /// Raises, and never returns (`RaiseException`, `RaiseLastException`). + Raises, +} + +/// Every function the runtime registers, by name, and what it does. +const BUILTINS: [(&str, Builtin); 59] = [ + ("IntToStr", Builtin::Pure), + ("StrToInt", Builtin::MayRaise), + ("StrToIntDef", Builtin::Pure), + ("Pos", Builtin::Pure), + ("Copy", Builtin::Pure), + ("Delete", Builtin::UpdatesReference), + ("Insert", Builtin::UpdatesReference), + ("StrGet", Builtin::MayRaise), + ("StrSet", Builtin::UpdatesReference), + ("UpperCase", Builtin::Pure), + ("LowerCase", Builtin::Pure), + ("Trim", Builtin::Pure), + ("Length", Builtin::Pure), + ("SetLength", Builtin::UpdatesReference), + ("Low", Builtin::Pure), + ("High", Builtin::Pure), + ("Dec", Builtin::UpdatesReference), + ("Inc", Builtin::UpdatesReference), + ("Include", Builtin::UpdatesReference), + ("Exclude", Builtin::UpdatesReference), + ("Sin", Builtin::Pure), + ("Cos", Builtin::Pure), + ("Sqrt", Builtin::MayRaise), + ("Round", Builtin::MayRaise), + ("Trunc", Builtin::MayRaise), + ("Int", Builtin::Pure), + ("Pi", Builtin::Pure), + ("Abs", Builtin::Pure), + ("StrToFloat", Builtin::MayRaise), + ("FloatToStr", Builtin::Pure), + ("PadL", Builtin::Pure), + ("PadR", Builtin::Pure), + ("PadZ", Builtin::Pure), + ("Replicate", Builtin::Pure), + ("StringOfChar", Builtin::Pure), + ("!ASSIGNED", Builtin::Pure), + ("Unassigned", Builtin::Pure), + ("VarIsEmpty", Builtin::Pure), + ("VarIsClear", Builtin::Pure), + ("Null", Builtin::Pure), + ("VarIsNull", Builtin::Pure), + ("VarType", Builtin::Pure), + ("GetArrayLength", Builtin::Pure), + ("SetArrayLength", Builtin::UpdatesReference), + ("RaiseLastException", Builtin::Raises), + ("RaiseException", Builtin::Raises), + ("ExceptionType", Builtin::ReadsException), + ("ExceptionParam", Builtin::ReadsException), + ("ExceptionProc", Builtin::ReadsException), + ("ExceptionPos", Builtin::ReadsException), + ("ExceptionToString", Builtin::Pure), + ("AnsiUpperCase", Builtin::Pure), + ("AnsiLowerCase", Builtin::Pure), + ("StrToInt64", Builtin::MayRaise), + ("Int64ToStr", Builtin::Pure), + ("StrToInt64Def", Builtin::Pure), + ("SizeOf", Builtin::Pure), + ("WStrGet", Builtin::MayRaise), + ("WStrSet", Builtin::UpdatesReference), +]; + +impl Builtin { + /// What the runtime function `name` does, or `None` when the runtime does + /// not register one by that name - a host's function, or a DLL import. + /// + /// # Arguments + /// + /// * `name` - The external procedure's name, in any case. + #[must_use] + pub fn of(name: &[u8]) -> Option { + BUILTINS + .iter() + .find(|(builtin, _)| builtin.as_bytes().eq_ignore_ascii_case(name)) + .map(|(_, behaviour)| *behaviour) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A name matches whatever its case, as the compiler writes it; a host's + /// function is not the runtime's. + #[test] + fn a_runtime_function_is_found_by_its_upper_cased_name() { + assert_eq!( + Builtin::of(b"SETARRAYLENGTH"), + Some(Builtin::UpdatesReference) + ); + assert_eq!(Builtin::of(b"IntToStr"), Some(Builtin::Pure)); + assert_eq!(Builtin::of(b"RAISEEXCEPTION"), Some(Builtin::Raises)); + assert_eq!( + Builtin::of(b"FILEEXISTS"), + None, + "Inno's, not the runtime's" + ); + } +} diff --git a/src/ty.rs b/src/ty.rs index 7591b81..13439d6 100644 --- a/src/ty.rs +++ b/src/ty.rs @@ -164,6 +164,126 @@ pub struct Type<'a> { pub attributes: Vec>, } +/// The machine a script's runtime was built for, as far as the memory layout +/// of its values depends on it: the size of a pointer, of a `Variant` and of +/// an `Extended`. +/// +/// A value's layout is the runtime's (`TPSTypeRec.CalcSize`, +/// `uPSRuntime.pas:1746-1790`): records are packed, each field at the sum of +/// the sizes before it (`LoadTypes`' `resolve`), a static array is its +/// elements back to back, and a dynamic array, a string and a class are a +/// pointer. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Machine { + /// A 32-bit runtime (`PointerSize = 4`), as Inno Setup's has been. + X86, + /// A 64-bit runtime (`CPU64`). + X64, +} + +/// How deep [`Machine::size`] follows records and arrays nested in each +/// other before refusing to answer: a type table the compiler writes nests +/// far less, and a hostile one may refer to itself. +const MAX_LAYOUT_DEPTH: u32 = 32; + +impl Machine { + /// The size of a pointer. + #[must_use] + pub const fn pointer_bytes(self) -> u32 { + match self { + Self::X86 => 4, + Self::X64 => 8, + } + } + + /// The size in memory of a value of type `type_no` (`RealSize`), or + /// `None` when the type table does not say: an index out of range, a type + /// nested past any the compiler writes, or a size that overflows. + /// + /// # Arguments + /// + /// * `types` - The container's type table. + /// * `type_no` - The type. + #[must_use] + pub fn size(self, types: &[Type<'_>], type_no: u32) -> Option { + self.size_within(types, type_no, MAX_LAYOUT_DEPTH) + } + + /// The offset of field `field` within a value of record type `record`, + /// or `None` when `record` is not a record or has no such field. + /// + /// # Arguments + /// + /// * `types` - The container's type table. + /// * `record` - The record type. + /// * `field` - The field, from 0. + #[must_use] + pub fn field_offset(self, types: &[Type<'_>], record: u32, field: u32) -> Option { + let TypeBody::Record { field_types } = &types.get(usize::try_from(record).ok()?)?.body + else { + return None; + }; + let before = field_types.get(..usize::try_from(field).ok()?)?; + if usize::try_from(field).ok()? >= field_types.len() { + return None; + } + before.iter().try_fold(0u32, |offset, field_type| { + offset.checked_add(self.size(types, *field_type)?) + }) + } + + /// [`Self::size`], following at most `depth` levels of nesting. + fn size_within(self, types: &[Type<'_>], type_no: u32, depth: u32) -> Option { + let ty = types.get(usize::try_from(type_no).ok()?)?; + let pointer = self.pointer_bytes(); + let nested = depth.checked_sub(1)?; + Some(match (&ty.base_type, &ty.body) { + (BaseType::Variant | BaseType::NotificationVariant, _) => match self { + Self::X86 => 16, + Self::X64 => 24, + }, + (BaseType::Char | BaseType::S8 | BaseType::U8, _) => 1, + (BaseType::WideChar | BaseType::S16 | BaseType::U16, _) => 2, + ( + BaseType::WideString + | BaseType::UnicodeString + | BaseType::Interface + | BaseType::Class + | BaseType::PChar + | BaseType::String + | BaseType::Array, + _, + ) => pointer, + (BaseType::Single | BaseType::S32 | BaseType::U32, _) => 4, + // A method pointer, and a pointer variable: the pointer, its type + // and whether it owns what it points to. + (BaseType::ProcPtr | BaseType::Pointer, _) => pointer.checked_mul(3)?, + (BaseType::Currency | BaseType::Double | BaseType::S64 | BaseType::U64, _) => 8, + (BaseType::Extended, _) => match self { + Self::X86 => 10, + Self::X64 => 8, + }, + // `TBTReturnAddress`: the procedure, then two `Cardinal`s. + (BaseType::ReturnAddress, _) => pointer.checked_add(8)?, + (BaseType::Set, TypeBody::Set { bit_size }) => bit_size.div_ceil(8), + ( + BaseType::StaticArray, + TypeBody::StaticArray { + element_type, size, .. + }, + ) => self + .size_within(types, *element_type, nested)? + .checked_mul(*size)?, + (BaseType::Record, TypeBody::Record { field_types }) => { + field_types.iter().try_fold(0u32, |total, field_type| { + total.checked_add(self.size_within(types, *field_type, nested)?) + })? + } + _ => 0, + }) + } +} + const HAS_EXPORT_NAME: u8 = 0x80; const MAX_NAME_LEN: u32 = 255; const MAX_SET_BIT_SIZE: u32 = 256; @@ -351,4 +471,68 @@ mod tests { other => panic!("expected Record, got {other:?}"), } } + + fn ty(base_type: BaseType, body: TypeBody<'static>) -> Type<'static> { + Type { + base_type, + body, + export_name: None, + attributes: Vec::new(), + } + } + + /// **A value is laid out as the runtime lays it out.** `code.iss`'s + /// `TCell` is two `Integer`s, packed: 8 bytes, `Y` at 4. A record of a + /// `Byte` then a `String` is packed too, so the string sits at 1. A + /// `TCounts = array[0..3] of Integer` is 16 bytes. A string is a pointer, + /// 4 bytes or 8 by the machine. + #[test] + fn a_value_is_laid_out_as_the_runtime_lays_it_out() { + let types = vec![ + ty(BaseType::S32, TypeBody::Bare), + ty(BaseType::U8, TypeBody::Bare), + ty(BaseType::String, TypeBody::Bare), + ty( + BaseType::Record, + TypeBody::Record { + field_types: vec![0, 0], + }, + ), + ty( + BaseType::Record, + TypeBody::Record { + field_types: vec![1, 2], + }, + ), + ty( + BaseType::StaticArray, + TypeBody::StaticArray { + element_type: 0, + size: 4, + start_offset: 0, + }, + ), + ]; + let x86 = Machine::X86; + assert_eq!(x86.size(&types, 3), Some(8)); + assert_eq!(x86.field_offset(&types, 3, 1), Some(4)); + assert_eq!(x86.field_offset(&types, 4, 1), Some(1), "packed"); + assert_eq!(x86.size(&types, 4), Some(5)); + assert_eq!(Machine::X64.size(&types, 4), Some(9)); + assert_eq!(x86.size(&types, 5), Some(16)); + assert_eq!(x86.field_offset(&types, 3, 2), None, "no third field"); + assert_eq!(x86.field_offset(&types, 0, 0), None, "not a record"); + } + + /// A record that contains itself has no size, rather than no end. + #[test] + fn a_record_that_contains_itself_has_no_size() { + let types = vec![ty( + BaseType::Record, + TypeBody::Record { + field_types: vec![0], + }, + )]; + assert_eq!(Machine::X86.size(&types, 0), None); + } }