Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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)"
Expand Down
95 changes: 94 additions & 1 deletion src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use crate::{
error::Error,
header::INVALID_VAL,
opcode::{Opcode, parse_opcode},
opcode::{ExceptionHandlerEnd, Opcode, parse_opcode},
reader::Reader,
ty::Type,
};
Expand Down Expand Up @@ -125,6 +125,99 @@ pub struct ProcDisasm<'a> {
pub instructions: Vec<Instruction<'a>>,
}

/// 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<u32>,
/// The except section, when the `try` has one.
pub except: Option<u32>,
/// The second finally section, when the `try` has one.
pub finally2: Option<u32>,
/// Where control continues once the `try` is done.
pub end: Option<u32>,
}

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<u32> {
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<Try> {
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<Try> {
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<u32> {
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
Expand Down
137 changes: 113 additions & 24 deletions src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<u32>>,
/// 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<Vec<Option<usize>>>,
}

/// The stack slots one call consumes: the top `count` of the caller's.
Expand Down Expand Up @@ -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<Option<u32>> = vec![None; instructions.len()];
let mut work = VecDeque::new();
let mut owners: Vec<Vec<Option<usize>>> = vec![Vec::new(); instructions.len()];
let mut work: VecDeque<(usize, Vec<Option<usize>>)> = 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<u32>, height: u32| {
let mut go = |target: Option<u32>, stack: &Vec<Option<usize>>| {
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));
Expand All @@ -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<usize> {
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
Expand Down Expand Up @@ -173,20 +223,29 @@ impl StackMap {
}
}

/// The height after `instruction` runs at `height`.
fn height_after(instruction: &Instruction<'_>, height: u32) -> Result<u32, Error> {
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<usize>],
) -> Result<Vec<Option<usize>>, 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()),
}
}

Expand Down Expand Up @@ -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");
}
}
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Loading
Loading