///|
fn copy_frames(frames : Array[Frame]) -> Array[Frame] {
frames.map(f => {
locals: f.locals.copy(),
stack: f.stack.copy(),
return_pc: f.return_pc,
store: f.store,
argc: f.argc,
})
}
///|
fn Machine::capture(self : Machine) -> Saved {
{
memory: Array::makei(self.static_base, i => self.mem[i]),
frames: copy_frames(self.frames),
pc: self.pc,
}
}
///|
fn Machine::apply_saved(self : Machine, saved : Saved) -> Unit {
// The transcript and fixed-pitch request bits survive restart and restore.
let flags = self.mem[17] & 3
for i in 0.. Unit {
data.push((value >> 8) & 255)
data.push(value & 255)
}
///|
fn append_long(data : Array[Int32], value : Int32) -> Unit {
append_word(data, value >> 16)
append_word(data, value)
}
///|
fn append_id(data : Array[Int32], id : String) -> Unit {
for c in id.to_array() {
data.push(c.to_int())
}
}
///|
fn append_chunk(
data : Array[Int32],
id : String,
payload : Array[Int32],
) -> Unit {
append_id(data, id)
append_long(data, payload.length())
for b in payload {
data.push(b)
}
if payload.length() % 2 != 0 {
data.push(0)
}
}
///|
/// Produce an interoperable Quetzal IFZS save while run() reports "save".
/// Uses UMem (uncompressed dynamic memory) and standard Stks call frames.
pub fn Machine::save_data(self : Machine) -> Bytes raise Fault {
if self.waiting != 3 {
raise Invalid("no pending save request")
}
let header : Array[Int32] = []
for i in [2, 3, 18, 19, 20, 21, 22, 23, 28, 29] {
header.push(self.original[i].to_int())
}
header.push((self.pc >> 16) & 255)
append_word(header, self.pc)
let stack : Array[Int32] = []
for i in 0..> 16) & 255)
append_word(stack, ret)
stack.push(f.locals.length() | (if i > 0 && f.store < 0 { 16 } else { 0 }))
stack.push(if f.store < 0 { 0 } else { f.store })
stack.push((1 << f.argc) - 1)
append_word(stack, f.stack.length())
for v in f.locals {
append_word(stack, v)
}
for v in f.stack {
append_word(stack, v)
}
}
let body : Array[Int32] = []
append_id(body, "IFZS")
append_chunk(body, "IFhd", header)
append_chunk(body, "UMem", Array::makei(self.static_base, i => self.mem[i]))
append_chunk(body, "Stks", stack)
let data : Array[Int32] = []
append_id(data, "FORM")
append_long(data, body.length())
for b in body {
data.push(b)
}
Bytes::from_array(data.map(i => i.to_byte()))
}
///|
/// Tell the interpreter whether its host persisted the pending save.
pub fn Machine::finish_save(self : Machine, success : Bool) -> Unit raise Fault {
if self.waiting != 3 {
raise Invalid("no pending save request")
}
self.waiting = 0
if self.version == 3 {
self.branch(success)
} else {
self.store(if success { 1 } else { 0 })
}
}
///|
/// Cancel an unavailable or declined restore operation.
pub fn Machine::cancel_restore(self : Machine) -> Unit raise Fault {
if self.waiting != 4 {
raise Invalid("no pending restore request")
}
self.waiting = 0
if self.version == 3 {
self.branch(false)
} else {
self.store(0)
}
}
///|
priv struct SaveReader {
bytes : Bytes
mut pos : Int32
limit : Int32
}
///|
fn SaveReader::byte(self : SaveReader) -> Int32 raise Fault {
if self.pos < 0 || self.pos >= self.limit {
raise Invalid("truncated Quetzal data")
}
let v = self.bytes[self.pos].to_int()
self.pos += 1
v
}
///|
fn SaveReader::word(self : SaveReader) -> Int32 raise Fault {
self.byte() * 256 + self.byte()
}
///|
fn SaveReader::long(self : SaveReader) -> Int32 raise Fault {
let hi = self.word()
let lo = self.word()
if hi >= 32768 {
raise Invalid("oversized Quetzal chunk")
}
hi * 65536 + lo
}
///|
fn SaveReader::id(self : SaveReader) -> String raise Fault {
let s = StringBuilder()
for _ in 0..<4 {
s.write_char(self.byte().unsafe_to_char())
}
s.to_string()
}
///|
/// Restore Quetzal CMem or UMem. The complete file is validated before changing state.
/// A rejected file leaves the pending restore available for cancellation or retry.
pub fn Machine::restore_data(self : Machine, data : Bytes) -> Unit raise Fault {
if self.waiting != 4 {
raise Invalid("no pending restore request")
}
let r = SaveReader::{ bytes: data, pos: 0, limit: data.length(), }
if r.id() != "FORM" {
raise Invalid("not an IFF FORM")
}
let length = r.long()
if length != data.length() - 8 || r.id() != "IFZS" {
raise Invalid("not a complete Quetzal FORM")
}
let mut pc = -1
let mut memory : Array[Int32]? = None
let mut frames : Array[Frame]? = None
while r.pos < r.limit {
let id = r.id()
let size = r.long()
if size > r.limit - r.pos {
raise Invalid("truncated Quetzal chunk")
}
let end = r.pos + size
let chunk = SaveReader::{ bytes: data, pos: r.pos, limit: end, }
match id {
"IFhd" => {
if pc >= 0 || size < 13 {
raise Invalid("invalid IFhd")
}
for i in [2, 3, 18, 19, 20, 21, 22, 23, 28, 29] {
if chunk.byte() != self.original[i].to_int() {
raise Invalid("save belongs to another story")
}
}
pc = chunk.byte() * 65536 + chunk.word()
if pc < 64 || pc >= self.mem.length() {
raise Invalid("invalid saved PC")
}
}
"UMem" | "CMem" => {
if pc < 0 || memory is Some(_) {
raise Invalid("invalid memory chunk order or duplicate")
}
let decoded = Array::makei(self.static_base, i => {
self.original[i].to_int()
})
if id == "UMem" {
if size != self.static_base {
raise Invalid("wrong UMem size")
}
for i in 0..= self.static_base {
raise Invalid("CMem exceeds dynamic memory")
}
decoded[i] = decoded[i] ^ value
i += 1
}
if i > self.static_base {
raise Invalid("CMem run exceeds dynamic memory")
}
}
}
// Immutable header addresses must still match the current story.
for i in [0, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 24, 25] {
if decoded[i] != self.original[i].to_int() {
raise Invalid("saved memory map differs")
}
}
memory = Some(decoded)
}
"Stks" => {
if pc < 0 || frames is Some(_) {
raise Invalid("invalid stack chunk")
}
let decoded : Array[Frame] = []
while chunk.pos < end {
if decoded.length() >= 4096 {
raise Invalid("too many saved call frames")
}
let ret = chunk.byte() * 65536 + chunk.word()
let flags = chunk.byte()
let dest = chunk.byte()
let supplied = chunk.byte()
let stack_count = chunk.word()
if (flags & 224) != 0 || supplied > 127 {
raise Invalid("invalid saved frame flags")
}
let locals : Array[Int32] = []
let stack : Array[Int32] = []
for _ in 0..<(flags & 15) {
locals.push(chunk.word())
}
for _ in 0..= self.mem.length() {
raise Invalid("invalid saved return PC")
}
let mut argc = 0
let mut bits = supplied
while bits > 0 {
argc += 1
bits = bits >> 1
}
decoded.push({
locals,
stack,
return_pc: ret,
store: if decoded.is_empty() || (flags & 16) != 0 {
-1
} else {
dest
},
argc,
})
}
if decoded.is_empty() {
raise Invalid("missing dummy frame")
}
frames = Some(decoded)
}
_ => ()
}
r.pos = end + size % 2
if r.pos > r.limit {
raise Invalid("missing IFF padding")
}
}
match (memory, frames) {
(Some(memory), Some(frames)) => {
let saved = Saved::{ memory, frames, pc, }
// Validate the resume store/branch on an isolated machine before committing.
let preview = Machine::new(self.original)
preview.apply_saved(saved)
if self.version == 3 {
preview.branch(true)
} else {
preview.store(2)
}
let _ = preview.byte(preview.pc)
self.apply_saved(preview.capture())
self.undo.clear()
}
_ => raise Invalid("missing required Quetzal chunks")
}
}