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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

All notable changes to this crate are documented here.

## 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.

- Added `signature`: `Signature` parses an internal procedure's export declaration (`InternalProc::signature()`) and an external one's bit string, and `ExternalDecl` a DLL import's library, function, calling convention and loading flags (`ExternalProc::declaration()`). `Signature::slot` says what a stack slot holds inside the procedure: the return address, the result reference, a parameter, or a local.
- Added `frame`: `StackMap` (`ProcDisasm::stack_map()`) gives the stack height before every instruction, following the body's control flow and restoring the height a `try` saved at each of its handler sections, and refuses a body whose paths disagree about it (`Error::StackImbalance`). `StackMap::call_slots` gives the slots a call consumes, from the callee's declaration or, for the compiler's intrinsics that declare no parameters, from the pops that follow the call. Checked on a compiled script: the two agree on every call to an internal procedure.
- Added `Opcode::operands()` and `Operand::var_refs()`: every operand an instruction carries, and every variable an operand names.
- Fixed `cm_spc` (`SetStackPointerToCopy`, opcode 22). It carries two variables, a destination pointer and the value it is made to point at a copy of, as the runtime reads them and the compiler writes them. It was decoded as a `u32`, which left the decoder mid-operand, so any procedure containing it failed to decode. The variant is now `SetStackPointerToCopy { dest, src }`.
- Fixed the coordinates of `flaggoto` and the four `PushExceptionHandler` sections. Like every other IFPS branch, they are deltas from the end of the instruction (the runtime adds the position after the operands), not procedure-local absolutes. On `imagemagick-setup.exe` the 0.1.2 reading put 10 of 14 `flaggoto` targets between instructions, and on a compiled `try` fixture all 4 exception sections. `Instruction::branch_targets()` now returns every form in IFPS-blob coordinates, so `ProcDisasm::branch_target()` / `branch_targets()` have no rebasing left to do and are removed. `FlagGoto`'s field is renamed `offset` to say it is a delta.
- An exception-handler section the `try` does not have (`INVALID_VAL` on the wire) is now `None` rather than a target one byte before the instruction.
- `FlagGoto` is a conditional branch in `Opcode::flow_type()`: it branches only when the saved flag is set.
- An unknown opcode byte is `Error::UnknownOpcode` and an unknown calculation, comparison or exception-section byte is `Error::UnknownSubOp`, both of which previously reported `Error::UnknownBaseType`.

## 0.1.3

- Replaced em-dashes and en-dashes in rustdoc comments and the changelog with plain ASCII hyphens, so generated documentation renders consistently.
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.1.3"
version = "0.2.0"
edition = "2024"
rust-version = "1.85"
description = "Read-only parser + disassembler for the RemObjects PascalScript III binary container format (IFPS)"
Expand Down
192 changes: 61 additions & 131 deletions src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use crate::{
error::Error,
header::INVALID_VAL,
opcode::{Opcode, parse_opcode},
reader::Reader,
ty::Type,
Expand Down Expand Up @@ -40,60 +41,45 @@ impl Instruction<'_> {
/// Conditional opcodes return their taken target. Exception-handler
/// setup opcodes carry multiple targets, so callers that need all of
/// them should use [`Self::branch_targets`].
///
/// Carries the same coordinate caveat as [`Self::branch_targets`] - prefer
/// [`ProcDisasm::branch_target`], which normalizes every form to IFPS-blob
/// coordinates.
pub fn branch_target(&self) -> Option<u32> {
self.branch_targets().into_iter().flatten().next()
}

/// Returns all explicit control-flow targets carried by this instruction.
///
/// # Coordinates
///
/// **The returned values are not all in the same coordinate system**, because
/// an [`Instruction`] does not know which procedure owns it:
/// Returns all explicit control-flow targets carried by this instruction,
/// in IFPS-blob coordinates - the same coordinate system as
/// [`Self::offset`], so a target can be compared directly against the
/// instruction stream.
///
/// - Relative branches (`goto`, `condgoto`, `condnotgoto`, the pop-and-goto
/// forms) resolve against [`Self::next_offset`], which is an IFPS-blob
/// offset - so these come back **blob-absolute**. A delta is
/// coordinate-independent, so this is exact.
/// - `flaggoto` and the four `PushExceptionHandler` regions store an
/// *absolute* position, and IFPS positions are **procedure-local** (the VM
/// indexes into the proc's own bytecode body). These come back exactly as
/// stored, i.e. relative to the owning proc's `bytecode_offset`.
/// Every IFPS branch stores a delta from the end of its instruction, and
/// the runtime adds it to the position after the operands: the gotos, the
/// pop-and-gotos, `flaggoto`, and each of the four exception-handler
/// sections. A delta is coordinate-independent, so resolving it against
/// [`Self::next_offset`] is exact. An exception-handler section the `try`
/// does not have is [`INVALID_VAL`] on the wire and `None` here.
///
/// Use [`ProcDisasm::branch_targets`] to get every form in blob
/// coordinates; it knows the proc base and applies it to the absolute forms
/// only.
/// The four slots are the handler's finally, except, second-finally and
/// end-of-block sections; a branch uses the first.
pub fn branch_targets(&self) -> [Option<u32>; 4] {
let at = |delta: u32| Some(relative_target(self.next_offset, delta));
let section =
|delta: u32| (delta != INVALID_VAL).then(|| relative_target(self.next_offset, delta));
match &self.opcode {
Opcode::Goto { offset }
| Opcode::PopAndGoto { offset }
| Opcode::Pop2AndGoto { offset } => [
Some(relative_target(self.next_offset, *offset as u32)),
None,
None,
None,
],
Opcode::CondGoto { offset, .. } | Opcode::CondNotGoto { offset, .. } => [
Some(relative_target(self.next_offset, *offset)),
None,
None,
None,
],
Opcode::FlagGoto { target } => [Some(*target), None, None, None],
| Opcode::Pop2AndGoto { offset } => [at(*offset as u32), None, None, None],
Opcode::CondGoto { offset, .. }
| Opcode::CondNotGoto { offset, .. }
| Opcode::FlagGoto { offset } => [at(*offset), None, None, None],
Opcode::PushExceptionHandler {
finally_offset,
exception_offset,
finally2_offset,
end_of_block,
} => [
Some(*finally_offset),
Some(*exception_offset),
Some(*finally2_offset),
Some(*end_of_block),
section(*finally_offset),
section(*exception_offset),
section(*finally2_offset),
section(*end_of_block),
],
_ => [None, None, None, None],
}
Expand All @@ -102,8 +88,9 @@ impl Instruction<'_> {

/// Resolves a PC-relative branch delta against the position it is relative to.
///
/// Every relative branch in IFPS (`Cm_G`, `Cm_CG`, `Cm_CNG`, and the two
/// pop-and-goto forms) encodes the same thing: a four-byte little-endian delta
/// Every IFPS branch (`Cm_G`, `Cm_CG`, `Cm_CNG`, `cm_fg`, the two
/// pop-and-goto forms and the exception-handler sections) encodes the same
/// thing: a four-byte little-endian delta
/// added to the position after the instruction. The VM performs that addition
/// on Delphi `Cardinal`s (`CurrentPosition := CurrentPosition + NewPosition`),
/// which wraps - so the compiler emits a **backward** branch as a large
Expand Down Expand Up @@ -138,44 +125,6 @@ pub struct ProcDisasm<'a> {
pub instructions: Vec<Instruction<'a>>,
}

impl ProcDisasm<'_> {
/// Returns `instruction`'s primary control-flow target in IFPS-blob
/// coordinates, or `None` for a non-branch opcode.
///
/// See [`Self::branch_targets`] for why this exists alongside
/// [`Instruction::branch_target`].
pub fn branch_target(&self, instruction: &Instruction<'_>) -> Option<u32> {
self.branch_targets(instruction)
.into_iter()
.flatten()
.next()
}

/// Returns every control-flow target `instruction` carries, all of them in
/// **IFPS-blob coordinates** - the same coordinate system as
/// [`Instruction::offset`], so a target can be compared directly against
/// the instruction stream.
///
/// [`Instruction::branch_targets`] cannot do this: `flaggoto` and the
/// `PushExceptionHandler` regions store procedure-local positions, and an
/// instruction does not know its procedure. This method adds
/// [`Self::bytecode_offset`] to exactly those forms and leaves the
/// already-blob-absolute relative branches alone.
///
/// `instruction` is expected to belong to this procedure; passing a foreign
/// one rebases its absolute targets against the wrong proc.
pub fn branch_targets(&self, instruction: &Instruction<'_>) -> [Option<u32>; 4] {
let raw = instruction.branch_targets();
if !matches!(
instruction.opcode,
Opcode::FlagGoto { .. } | Opcode::PushExceptionHandler { .. }
) {
return raw;
}
raw.map(|target| target.map(|t| self.bytecode_offset.wrapping_add(t)))
}
}

/// Decodes the bytecode body for an internal proc.
///
/// `blob` is the entire IFPS byte buffer; `bytecode_offset` and
Expand Down Expand Up @@ -306,88 +255,69 @@ mod tests {
assert_eq!(unconditional.branch_target(), conditional.branch_target());
}

/// `flaggoto` carries an absolute position, not a delta - and on the bare
/// instruction it comes back procedure-local, as stored.
/// `flaggoto` carries a delta from the end of the instruction, like every
/// other branch: the runtime adds it to the position after the operand.
/// Read as a procedure-local absolute (the 0.1.2 reading) it resolved to a
/// boundary for 4 of the 14 in `imagemagick-setup.exe`; as a delta, 14.
#[test]
fn flag_goto_target_is_absolute() {
let inst = at(0x0100, Opcode::FlagGoto { target: 0x40 });
assert_eq!(inst.branch_target(), Some(0x40));
}

/// Builds a single-instruction [`ProcDisasm`] based at `bytecode_offset`.
fn proc(bytecode_offset: u32, inst: Instruction<'static>) -> ProcDisasm<'static> {
ProcDisasm {
proc_index: 0,
bytecode_offset,
instructions: vec![inst],
}
fn flag_goto_target_is_relative_to_the_next_instruction() {
let inst = at(0x1100, Opcode::FlagGoto { offset: 0x40 });
assert_eq!(inst.branch_target(), Some(0x1140));
let back = at(
0x1100,
Opcode::FlagGoto {
offset: (-0x40i32) as u32,
},
);
assert_eq!(back.branch_target(), Some(0x10C0), "a backward delta wraps");
}

/// The proc-level resolver rebases procedure-local absolute targets onto
/// the proc's blob offset, so they can be compared against instruction
/// offsets from the same listing.
/// Non-branch opcodes carry no targets at all.
#[test]
fn proc_level_resolution_rebases_absolute_targets() {
let inst = at(0x1100, Opcode::FlagGoto { target: 0x40 });
let disasm = proc(0x1000, inst.clone());
assert_eq!(inst.branch_target(), Some(0x40), "raw form stays as stored");
assert_eq!(disasm.branch_target(&inst), Some(0x1040));
fn non_branch_opcodes_have_no_targets() {
let inst = at(0x0100, Opcode::Return);
assert_eq!(inst.branch_target(), None);
assert_eq!(inst.branch_targets(), [None, None, None, None]);
}

/// All four exception-handler regions are rebased.
/// The exception-handler setup opcode carries four targets, each a delta
/// from the end of the instruction; `branch_target` surfaces the first.
#[test]
fn proc_level_resolution_rebases_every_handler_region() {
fn exception_handler_carries_every_region_target() {
let inst = at(
0x1100,
0x0100,
Opcode::PushExceptionHandler {
finally_offset: 0x10,
exception_offset: 0x20,
finally2_offset: 0x30,
end_of_block: 0x40,
},
);
let disasm = proc(0x1000, inst.clone());
assert_eq!(
disasm.branch_targets(&inst),
[Some(0x1010), Some(0x1020), Some(0x1030), Some(0x1040)],
inst.branch_targets(),
[Some(0x0110), Some(0x0120), Some(0x0130), Some(0x0140)],
);
assert_eq!(inst.branch_target(), Some(0x0110));
}

/// Relative branches are already blob-absolute, so the proc-level resolver
/// must leave them untouched rather than double-adding the proc base.
#[test]
fn proc_level_resolution_leaves_relative_targets_alone() {
let inst = at(0x1100, Opcode::Goto { offset: -0x40 });
let disasm = proc(0x1000, inst.clone());
assert_eq!(disasm.branch_target(&inst), inst.branch_target());
assert_eq!(disasm.branch_target(&inst), Some(0x10C0));
}

/// Non-branch opcodes carry no targets at all.
#[test]
fn non_branch_opcodes_have_no_targets() {
let inst = at(0x0100, Opcode::Return);
assert_eq!(inst.branch_target(), None);
assert_eq!(inst.branch_targets(), [None, None, None, None]);
}

/// The exception-handler setup opcode carries four targets; `branch_target`
/// surfaces the first.
/// A section the `try` does not have is `INVALID_VAL` on the wire, which
/// the runtime skips rather than adds; it is no target at all, where
/// resolving it as a delta would land one byte before the instruction.
#[test]
fn exception_handler_carries_every_region_target() {
fn an_absent_handler_section_is_no_target() {
let inst = at(
0x0100,
Opcode::PushExceptionHandler {
finally_offset: 0x10,
finally_offset: INVALID_VAL,
exception_offset: 0x20,
finally2_offset: 0x30,
finally2_offset: INVALID_VAL,
end_of_block: 0x40,
},
);
assert_eq!(
inst.branch_targets(),
[Some(0x10), Some(0x20), Some(0x30), Some(0x40)],
[None, Some(0x0120), None, Some(0x0140)],
);
assert_eq!(inst.branch_target(), Some(0x10));
assert_eq!(inst.branch_target(), Some(0x0120));
}
}
Loading
Loading