Compare commits
4 Commits
3df54bfa52
...
4f4e27e03c
Author | SHA1 | Date |
---|---|---|
aprzn | 4f4e27e03c | 7 months ago |
aprzn | 123792a0df | 7 months ago |
aprzn | abb9713fce | 7 months ago |
aprzn | ffe9706f73 | 7 months ago |
@ -1,8 +1,5 @@
|
||||
[package]
|
||||
name = "zing"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"zing", "zingprocmacros",
|
||||
]
|
||||
|
@ -1 +1,3 @@
|
||||
# ZING: Z-machine Implementation for ruNning Games
|
||||
|
||||
to use for tests when ready: https://github.com/jeffnyman/zifmia
|
||||
|
@ -1,85 +0,0 @@
|
||||
/// 5 bits
|
||||
#[derive(Clone, Copy)]
|
||||
struct ZChar(u8);
|
||||
|
||||
/// technically 10 bits, but top two unused so they are dropped
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub struct ZsciiChar(u8);
|
||||
|
||||
pub type ZsciiString = Vec<ZsciiChar>;
|
||||
|
||||
/// Returns:
|
||||
/// - a result that wraps a ZsciiString, erroring if the slice terminates before the string ends
|
||||
/// - a usize indicating how many bytes were consumed
|
||||
pub fn decode_zchars(
|
||||
zchars: &[u8],
|
||||
alphabet_table: Option<&[u8]>,
|
||||
abbreviations_table: &[u8],
|
||||
) -> Option<(ZsciiString, usize)> {
|
||||
fn cut_string(zchars: &[u8]) -> Option<Vec<u16>> {
|
||||
let mut out = Vec::new();
|
||||
for word in zchars.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])) {
|
||||
out.push(word);
|
||||
if 0x8000 & word != 0 {
|
||||
return Some(out);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// requires: alphabet_num < 3, 5 < char_idx < 32, !(alphabet_num = 2 AND char_idx = 6)
|
||||
fn index_alphabet(
|
||||
alphabet_table: Option<&[u8]>,
|
||||
alphabet_number: usize,
|
||||
ZChar(char_idx): ZChar,
|
||||
zchars: &mut impl Iterator<Item = ZChar>,
|
||||
) -> Option<ZsciiChar> {
|
||||
let default_alphabet: [[ZsciiChar; 26]; 3] = [
|
||||
b"abcdefghijklmnopqrstuvwxyz".map(ZsciiChar),
|
||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ".map(ZsciiChar),
|
||||
br#" 0123456789.,!?_#'"/\-:()"#.map(ZsciiChar)
|
||||
];
|
||||
if alphabet_number == 2 && char_idx == 7 {
|
||||
Some(ZsciiChar(13))
|
||||
} else if alphabet_number == 2 && char_idx == 6 {
|
||||
todo!()
|
||||
} else if let Some(alphabet_table) = alphabet_table {
|
||||
todo!()
|
||||
} else {
|
||||
Some(default_alphabet[alphabet_number][char_idx as usize - 6])
|
||||
}
|
||||
}
|
||||
|
||||
let zwords = cut_string(zchars)?;
|
||||
let consumed_length = zwords.len() * 2;
|
||||
let mut zchars = zwords.iter()
|
||||
.flat_map(|word| [
|
||||
(word >> 10) & 0x1f,
|
||||
(word >> 5) & 0x1f,
|
||||
word & 0x1f
|
||||
])
|
||||
.map(|word| ZChar(word as u8))
|
||||
.peekable();
|
||||
|
||||
let mut out = Vec::new();
|
||||
let mut current_alphabet = 0;
|
||||
while let Some(char) = zchars.next() {
|
||||
match char {
|
||||
ZChar(0) => out.push(ZsciiChar(32)),
|
||||
ZChar(1..=3) => todo!("abbreviations"),
|
||||
ZChar(4..=5) => if zchars.peek().is_some_and(|&ZChar(n)| n > 5) {
|
||||
if let Some(zc) = index_alphabet(alphabet_table, char.0 as usize - 3, zchars.next().unwrap(), &mut zchars) { out.push(zc) }
|
||||
},
|
||||
ZChar(_) => if let Some(zc) = index_alphabet(alphabet_table, 0, char, &mut zchars) {
|
||||
out.push(zc)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((out, consumed_length))
|
||||
}
|
||||
|
||||
|
||||
/// Returns:
|
||||
pub fn decode_instruction(memory: &[u8], address: usize) {
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "zing"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
zingprocmacros = { path = "../zingprocmacros" }
|
@ -0,0 +1,33 @@
|
||||
pub mod instruction;
|
||||
pub mod text;
|
||||
|
||||
// see §11 (page 61) of the spec
|
||||
pub mod header_fields {
|
||||
const VERSION: usize = 0x0;
|
||||
const FLAGS1: usize = 0x1;
|
||||
const HIGH_MEMORY_START_ADDR: usize = 0x4;
|
||||
const PC_INIT_VAL_ADDR: usize = 0x6;
|
||||
const DICTIONARY_ADDR: usize = 0x8;
|
||||
const OBJECT_TABLE_ADDR: usize = 0xA;
|
||||
const GLOBALS_TABLE_ADDR: usize = 0xC;
|
||||
const STATIC_MEMORY_START_ADDR: usize = 0xE;
|
||||
const FLAGS2: usize = 0x10;
|
||||
const ABBREVIATIONS_TABLE_ADDR: usize = 0x18;
|
||||
const FILE_LENGTH: usize = 0x1A; // divided by a constant; see §11.1.6
|
||||
const FILE_CHECKSUM: usize = 0x1C;
|
||||
const INTERPRETER_NUMBER: usize = 0x1E;
|
||||
const INTERPRETER_VERSION: usize = 0x1F;
|
||||
const SCREEN_HEIGHT_CHARS: usize = 0x20;
|
||||
const SCREEN_WIDTH_CHARS: usize = 0x21;
|
||||
const SCREEN_HEIGHT_UNITS: usize = 0x22;
|
||||
const SCREEN_WIDTH_UNITS: usize = 0x24;
|
||||
const FONT_WIDTH_UNITS: usize = 0x26;
|
||||
const FONT_HEIGHT_UNITS: usize = 0x27;
|
||||
const BACKGROUND_COLOR: usize = 0x2C;
|
||||
const FOREGROUND_COLOR: usize = 0x2D;
|
||||
const TERMINATING_CHARS_TABLE_ADDR: usize = 0x2E;
|
||||
const REVISION_NUMBER: usize = 0x32;
|
||||
const ALPHABET_TABLE_ADDR: usize = 0x34;
|
||||
const HEADER_EXTENSION_TABLE_ADDR: usize = 0x36;
|
||||
}
|
||||
|
@ -0,0 +1,363 @@
|
||||
use zingprocmacros::make_instruction_type;
|
||||
use crate::{encoding::text::{decode_zchars, ZsciiString}, utils::simplecursor::SimpleCursor};
|
||||
|
||||
trait Opcode: Sized {
|
||||
fn has_store(inst_no: u8) -> bool;
|
||||
fn has_branch(inst_no: u8) -> bool;
|
||||
fn has_text(inst_no: u8) -> bool;
|
||||
fn from_number(inst_no: u8, store: Option<u8>, branch: Option<Branch>, text: Option<ZsciiString>) -> Result<Self, InstructionDecodeError>;
|
||||
}
|
||||
|
||||
pub enum Instruction {
|
||||
Short0Op {
|
||||
opcode: Short0OpOpcode,
|
||||
},
|
||||
Short1Op {
|
||||
operand: Operand,
|
||||
opcode: Short1OpOpcode,
|
||||
},
|
||||
Long2Op {
|
||||
operands: (Operand, Operand),
|
||||
opcode: Long2OpOpcode,
|
||||
},
|
||||
Variable2Op {
|
||||
operands: (Operand, Operand),
|
||||
opcode: Variable2OpOpcode,
|
||||
},
|
||||
VariableVarOp {
|
||||
operands: Vec<Operand>,
|
||||
opcode: VariableVarOpOpcode
|
||||
},
|
||||
ExtendedVarOp {
|
||||
operands: Vec<Operand>,
|
||||
opcode: ExtendedVarOpOpcode,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum Operand {
|
||||
SmallConstant(u8),
|
||||
LargeConstant(u16),
|
||||
Variable(u8),
|
||||
}
|
||||
|
||||
impl Operand {
|
||||
fn make_small(input: &[u8; 1]) -> Self {Self::SmallConstant(input[0])}
|
||||
fn make_large(input: &[u8; 2]) -> Self {Self::LargeConstant(u16::from_be_bytes(*input))}
|
||||
fn make_var(input: &[u8; 1]) -> Self {Self::Variable(input[0])}
|
||||
}
|
||||
|
||||
enum BranchTarget {
|
||||
ReturnsFalse,
|
||||
ReturnsTrue,
|
||||
ShiftedOffset(i16),
|
||||
}
|
||||
|
||||
struct Branch {
|
||||
branch_if_true: bool,
|
||||
target: BranchTarget
|
||||
}
|
||||
|
||||
make_instruction_type!(Short0OpOpcode {
|
||||
0x0 Rtrue,
|
||||
0x1 Rfalse,
|
||||
0x2 Print txt,
|
||||
0x3 PrintRet txt,
|
||||
0x4 Nop,
|
||||
0x7 Restart,
|
||||
0x8 RetPopped,
|
||||
0x9 Catch st,
|
||||
0xA Quit,
|
||||
0xB NewLine,
|
||||
0xD Verify,
|
||||
0xF Piracy
|
||||
});
|
||||
|
||||
make_instruction_type!(Short1OpOpcode {
|
||||
0x0 Jz br,
|
||||
0x1 GetSibling st br,
|
||||
0x2 GetChild st br,
|
||||
0x3 GetParent st,
|
||||
0x4 GetPropLen st,
|
||||
0x5 Inc,
|
||||
0x6 Dec,
|
||||
0x7 PrintAddr,
|
||||
0x8 Call1s st,
|
||||
0x9 RemoveObj,
|
||||
0xA PrintObj,
|
||||
0xB Ret,
|
||||
0xC Jump,
|
||||
0xD PrintPaddr,
|
||||
0xE Load st,
|
||||
0xF Call1n,
|
||||
});
|
||||
|
||||
make_instruction_type!(Variable2OpOpcode {
|
||||
1 Je br,
|
||||
2 Jl br,
|
||||
3 Jg br,
|
||||
4 DecChk br,
|
||||
5 IncChk br,
|
||||
6 Jin br,
|
||||
7 Test br,
|
||||
8 Or st,
|
||||
9 And st,
|
||||
10 TestAttr br,
|
||||
11 SetAttr,
|
||||
12 ClearAtr,
|
||||
13 Store,
|
||||
14 InsertObj,
|
||||
15 Loadw st,
|
||||
16 Loadb st,
|
||||
17 GetProp st,
|
||||
18 GetPropAddr st,
|
||||
19 GetNextProp st,
|
||||
20 Add st,
|
||||
21 Sub st,
|
||||
22 Mul st,
|
||||
23 Div st,
|
||||
24 Mod st,
|
||||
25 Call2s st,
|
||||
26 Call2n,
|
||||
27 SetColour,
|
||||
28 Throw,
|
||||
});
|
||||
|
||||
type Long2OpOpcode = Variable2OpOpcode;
|
||||
|
||||
make_instruction_type!(VariableVarOpOpcode {
|
||||
0 CallVs,
|
||||
1 Storew,
|
||||
2 Storeb,
|
||||
3 PutProp,
|
||||
4 Aread st,
|
||||
5 PrintChar,
|
||||
6 PrintNum,
|
||||
7 Random st,
|
||||
8 Push,
|
||||
9 Pull,
|
||||
10 SplitWindow,
|
||||
11 SetWindow,
|
||||
12 CallVs2 st,
|
||||
13 EraseWindow,
|
||||
14 EraseLine,
|
||||
15 SetCursor,
|
||||
16 GetCursor,
|
||||
17 SetTextStyle,
|
||||
18 BufferMode,
|
||||
19 OutputStream,
|
||||
20 InputStream,
|
||||
21 SoundEffect,
|
||||
22 ReadChar st,
|
||||
23 ScanTable st br,
|
||||
24 Not st,
|
||||
25 CallVn,
|
||||
26 CallVn2,
|
||||
27 Tokenise,
|
||||
28 EncodeText,
|
||||
29 CopyTable,
|
||||
30 PrintTable,
|
||||
31 CheckArgCount br,
|
||||
});
|
||||
make_instruction_type!(ExtendedVarOpOpcode {
|
||||
0 Save st,
|
||||
1 Restore st,
|
||||
2 LogShift st,
|
||||
3 ArtShift st,
|
||||
4 SetFont st,
|
||||
5 SaveUndo st,
|
||||
6 RestoreUndo st,
|
||||
11 PrintUnicode,
|
||||
12 CheckUnicode,
|
||||
13 SetTrueColor,
|
||||
});
|
||||
|
||||
|
||||
pub enum InstructionDecodeError {
|
||||
InvalidOpcodeNum(u8),
|
||||
/// Should not occur unless there is an error in Zing
|
||||
MissingStore,
|
||||
/// Should not occur unless there is an error in Zing
|
||||
MissingBranch,
|
||||
/// Should not occur unless there is an error in Zing
|
||||
MissingText,
|
||||
InsufficientOperands,
|
||||
}
|
||||
|
||||
pub fn decode_instruction(cursor: &mut SimpleCursor) -> Result<Instruction, InstructionDecodeError> {
|
||||
struct HasStoreBranchText { store: bool, branch: bool, text: bool }
|
||||
struct PotentialStoreBranchText {
|
||||
store: Option<u8>,
|
||||
branch: Option<Branch>,
|
||||
text: Option<ZsciiString>,
|
||||
}
|
||||
|
||||
fn decode_store_branch_text<T: Opcode>(
|
||||
cursor: &mut SimpleCursor,
|
||||
opcode_num: u8
|
||||
) -> PotentialStoreBranchText {
|
||||
let required = HasStoreBranchText {
|
||||
store: T::has_store(opcode_num),
|
||||
branch: T::has_branch(opcode_num),
|
||||
text: T::has_text(opcode_num)
|
||||
};
|
||||
let store = required.store.then(|| cursor.read_const::<1>().unwrap()[0]);
|
||||
let branch = required.branch.then(|| {
|
||||
let &[branch_first_byte] = cursor.read_const::<1>().unwrap();
|
||||
let branch_if_true = branch_first_byte & 0x80 != 0;
|
||||
let branch_target_raw_high = branch_first_byte & 0x3f;
|
||||
let branch_target_raw = if branch_first_byte & 0x40 == 0 {
|
||||
// one byte
|
||||
branch_target_raw_high as i16
|
||||
} else {
|
||||
// two bytes
|
||||
let &[branch_target_raw_low] = cursor.read_const::<1>().unwrap();
|
||||
let branch_target_raw_unsigned = i16::from_be_bytes([branch_target_raw_high, branch_target_raw_low]);
|
||||
(branch_target_raw_unsigned << 2) / 4
|
||||
};
|
||||
match branch_target_raw {
|
||||
0 => Branch { branch_if_true, target: BranchTarget::ReturnsFalse },
|
||||
1 => Branch { branch_if_true, target: BranchTarget::ReturnsTrue },
|
||||
n => Branch { branch_if_true, target: BranchTarget::ShiftedOffset(n - 2) },
|
||||
}
|
||||
});
|
||||
// TODO: switch away from cursor.buf(); should not assume that the cursor is looking at the
|
||||
// entire slice
|
||||
let extra_cursor = cursor.clone();
|
||||
let mem = extra_cursor.buf();
|
||||
let alphabet_addr = u16::from_be_bytes([mem[0x08], mem[0x09]]) as usize;
|
||||
let abbreviations_addr = u16::from_be_bytes([mem[0x18], mem[0x18]]) as usize;
|
||||
let text = required.branch.then(|| decode_zchars(cursor, alphabet_addr, abbreviations_addr, mem).unwrap());
|
||||
PotentialStoreBranchText { store, branch, text }
|
||||
}
|
||||
|
||||
fn decode_short(cursor: &mut SimpleCursor) -> Result<Instruction, InstructionDecodeError> {
|
||||
let &[base] = cursor.read_const::<1>().unwrap();
|
||||
let opcode_num = base & 0x0f;
|
||||
let operand_desc = (base >> 4) & 0b11;
|
||||
let operand = match operand_desc {
|
||||
0b00 => Some(Operand::make_large(cursor.read_const().unwrap())),
|
||||
0b01 => Some(Operand::make_small(cursor.read_const().unwrap())),
|
||||
0b10 => Some(Operand::make_var(cursor.read_const().unwrap())),
|
||||
0b11 => None,
|
||||
_ => unreachable!()
|
||||
};
|
||||
let PotentialStoreBranchText { store, branch, text } = if operand.is_none() {
|
||||
decode_store_branch_text::<Short0OpOpcode>(cursor, opcode_num)
|
||||
} else {
|
||||
decode_store_branch_text::<Short1OpOpcode>(cursor, opcode_num)
|
||||
};
|
||||
|
||||
if let Some(operand) = operand {
|
||||
Short1OpOpcode::from_number(opcode_num, store, branch, text)
|
||||
.map(|opcode| Instruction::Short1Op { operand, opcode })
|
||||
} else {
|
||||
Short0OpOpcode::from_number(opcode_num, store, branch, text)
|
||||
.map(|opcode| Instruction::Short0Op { opcode })
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_long(cursor: &mut SimpleCursor) -> Result<Instruction, InstructionDecodeError> {
|
||||
enum OperandType { SmallConst, Var }
|
||||
impl From<bool> for OperandType {
|
||||
fn from(b: bool) -> Self {if b {Self::Var} else {Self::SmallConst}}
|
||||
}
|
||||
let &[base] = cursor.read_const().unwrap();
|
||||
let opcode_num = base & 0x1f;
|
||||
let op1_type = OperandType::from(base & 0x40 != 0);
|
||||
let op2_type = OperandType::from(base & 0x20 != 0);
|
||||
let operand_1 = match op1_type {
|
||||
OperandType::SmallConst => Operand::make_small(cursor.read_const().unwrap()),
|
||||
OperandType::Var => Operand::make_var(cursor.read_const().unwrap()),
|
||||
};
|
||||
let operand_2 = match op2_type {
|
||||
OperandType::SmallConst => Operand::make_small(cursor.read_const().unwrap()),
|
||||
OperandType::Var => Operand::make_var(cursor.read_const().unwrap()),
|
||||
};
|
||||
let PotentialStoreBranchText { store, branch, text } = decode_store_branch_text::<Long2OpOpcode>(cursor, opcode_num);
|
||||
Long2OpOpcode::from_number(opcode_num, store, branch,text)
|
||||
.map(|opcode| Instruction::Long2Op { operands: (operand_1, operand_2), opcode })
|
||||
}
|
||||
|
||||
fn decode_extended(cursor: &mut SimpleCursor) -> Result<Instruction, InstructionDecodeError> {
|
||||
let _ = cursor.read_const::<1>();
|
||||
let &[opcode_num] = cursor.read_const().unwrap();
|
||||
let &[op_descs] = cursor.read_const().unwrap();
|
||||
let operands = {
|
||||
let mut operands = vec![];
|
||||
for i in 0..4 {
|
||||
match (op_descs >> (6 - 2 * i)) & 0b11 {
|
||||
0b00 => operands.push(Operand::make_large(cursor.read_const().unwrap())),
|
||||
0b01 => operands.push(Operand::make_small(cursor.read_const().unwrap())),
|
||||
0b10 => operands.push(Operand::make_var(cursor.read_const().unwrap())),
|
||||
0b11 => (),
|
||||
_ => unreachable!()
|
||||
};
|
||||
}
|
||||
operands
|
||||
};
|
||||
let PotentialStoreBranchText { store, branch, text } = decode_store_branch_text::<ExtendedVarOpOpcode>(cursor, opcode_num);
|
||||
ExtendedVarOpOpcode::from_number(opcode_num, store, branch, text)
|
||||
.map(|opcode| Instruction::ExtendedVarOp { operands, opcode })
|
||||
}
|
||||
|
||||
fn decode_variable(cursor: &mut SimpleCursor) -> Result<Instruction, InstructionDecodeError> {
|
||||
const CALL_VS2_OPCODE_NUM: u8 = 12;
|
||||
const CALL_VN2_OPCODE_NUM: u8 = 26;
|
||||
let &[base] = cursor.read_const().unwrap();
|
||||
let opcode_num = base & 0x1f;
|
||||
let is_varop = base & 0x20 != 0;
|
||||
if is_varop {
|
||||
let mut operands = vec![];
|
||||
let num_operand_desc_bytes =
|
||||
if opcode_num == CALL_VN2_OPCODE_NUM
|
||||
|| opcode_num == CALL_VS2_OPCODE_NUM
|
||||
{2} else {1};
|
||||
for _ in 0..num_operand_desc_bytes {
|
||||
let &[op_descs] = cursor.read_const().unwrap();
|
||||
for i in 0..4 {
|
||||
match (op_descs >> (6 - 2 * i)) & 0b11 {
|
||||
0b00 => operands.push(Operand::make_large(cursor.read_const().unwrap())),
|
||||
0b01 => operands.push(Operand::make_small(cursor.read_const().unwrap())),
|
||||
0b10 => operands.push(Operand::make_var(cursor.read_const().unwrap())),
|
||||
0b11 => (),
|
||||
_ => unreachable!()
|
||||
};
|
||||
}
|
||||
}
|
||||
let PotentialStoreBranchText { store, branch, text } = decode_store_branch_text::<VariableVarOpOpcode>(cursor, opcode_num);
|
||||
VariableVarOpOpcode::from_number(opcode_num, store, branch, text)
|
||||
.map(|opcode| Instruction::VariableVarOp { operands, opcode })
|
||||
} else {
|
||||
let &[op_descs] = cursor.read_const().unwrap();
|
||||
let op0 = match (op_descs >> 6) & 0b11 {
|
||||
0b00 => Operand::make_large(cursor.read_const().unwrap()),
|
||||
0b01 => Operand::make_small(cursor.read_const().unwrap()),
|
||||
0b10 => Operand::make_var(cursor.read_const().unwrap()),
|
||||
0b11 => return Err(InstructionDecodeError::InsufficientOperands),
|
||||
_ => unreachable!()
|
||||
};
|
||||
let op1 = match (op_descs >> 4) & 0b11 {
|
||||
0b00 => Operand::make_large(cursor.read_const().unwrap()),
|
||||
0b01 => Operand::make_small(cursor.read_const().unwrap()),
|
||||
0b10 => Operand::make_var(cursor.read_const().unwrap()),
|
||||
0b11 => return Err(InstructionDecodeError::InsufficientOperands),
|
||||
_ => unreachable!()
|
||||
};
|
||||
let operands = (op0, op1);
|
||||
let PotentialStoreBranchText { store, branch, text } = decode_store_branch_text::<Variable2OpOpcode>(cursor, opcode_num);
|
||||
Variable2OpOpcode::from_number(opcode_num, store, branch, text)
|
||||
.map(|opcode| Instruction::Variable2Op { operands, opcode })
|
||||
}
|
||||
}
|
||||
|
||||
let &[first_byte] = cursor.peek_const::<1>().unwrap();
|
||||
if first_byte == 190 {
|
||||
decode_extended(cursor)
|
||||
} else if first_byte & 0xC0 == 0xC0 {
|
||||
decode_variable(cursor)
|
||||
} else if first_byte & 0xC0 == 0x80 {
|
||||
decode_short(cursor)
|
||||
} else {
|
||||
decode_long(cursor)
|
||||
}
|
||||
}
|
@ -0,0 +1,373 @@
|
||||
use crate::utils::simplecursor::SimpleCursor;
|
||||
|
||||
/// 5 bits
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
struct ZChar(u8);
|
||||
|
||||
/// technically 10 bits, but top two unused so they are dropped
|
||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||
pub struct ZsciiChar(u8);
|
||||
|
||||
pub type ZsciiString = Vec<ZsciiChar>;
|
||||
|
||||
fn zscii_from_bytes(bytes: &[u8]) -> ZsciiString {
|
||||
bytes.into_iter().cloned().map(ZsciiChar).collect()
|
||||
}
|
||||
|
||||
/// Returns:
|
||||
/// - a result that wraps a ZsciiString, erroring if the slice terminates before the string ends
|
||||
/// - a usize indicating how many bytes were consumed
|
||||
pub fn decode_zchars(
|
||||
zchars: &mut SimpleCursor,
|
||||
alphabet_table_addr: usize,
|
||||
abbreviations_table_addr: usize,
|
||||
memory: &[u8],
|
||||
) -> Option<ZsciiString> {
|
||||
fn get_from_alphabet(alphabet_number: usize, ZChar(codepoint): ZChar, alphabet_table: Option<&[u8]>) -> ZsciiChar {
|
||||
|
||||
ZsciiChar(
|
||||
alphabet_table
|
||||
// ~'s indicate invalid characters (reserved values A2:6 and A2:7)
|
||||
.unwrap_or(br#"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~~0123456789.,!?_#'"/\-:()"#)
|
||||
[26 * alphabet_number + codepoint as usize - 6]
|
||||
)
|
||||
}
|
||||
|
||||
struct ZsciiSequence<'a, T>
|
||||
where T: Iterator<Item = ZChar> {
|
||||
zchars: std::iter::Peekable<T>,
|
||||
subseq: Option<Box<ZsciiSequence<'a, T>>>,
|
||||
alphabet_number: usize,
|
||||
alphabet_table: Option<&'a [u8]>
|
||||
}
|
||||
impl<'a, T> Iterator for ZsciiSequence<'a, T>
|
||||
where T: Iterator<Item = ZChar> {
|
||||
type Item = ZsciiChar;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.subseq.as_mut() {
|
||||
Some(subseq) => {
|
||||
let out = subseq.next();
|
||||
if subseq.next().is_none() { self.subseq = None; }
|
||||
out
|
||||
},
|
||||
None => {
|
||||
let char = self.zchars.next()?;
|
||||
match char {
|
||||
ZChar(0) => Some(ZsciiChar(32)),
|
||||
ZChar(1..=3) => {
|
||||
self.subseq = Some(Box::new(ZsciiSequence {
|
||||
zchars: todo!("abbreviations"),
|
||||
subseq: None,
|
||||
alphabet_number: self.alphabet_number,
|
||||
alphabet_table: self.alphabet_table
|
||||
}));
|
||||
self.next()
|
||||
},
|
||||
ZChar(4..=5) => {
|
||||
if char == ZChar(5) && self.zchars.peek() == Some(&ZChar(6)) {
|
||||
Some(ZsciiChar(13))
|
||||
} else if char == ZChar(5) && self.zchars.peek() == Some(&ZChar(7)) {
|
||||
let _ = self.zchars.next()?;
|
||||
let ZChar(z0) = self.zchars.next()?;
|
||||
let ZChar(z1) = self.zchars.next()?;
|
||||
Some(ZsciiChar((z0 << 5) | z1))
|
||||
} else {
|
||||
self.alphabet_number = char.0 as usize - 3;
|
||||
let out = self.next();
|
||||
self.alphabet_number = 0;
|
||||
out
|
||||
}
|
||||
},
|
||||
ZChar(_) => Some(get_from_alphabet(self.alphabet_number, char, self.alphabet_table))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ZWordIter<'a, 'b> {
|
||||
cursor: &'a mut SimpleCursor<'b>,
|
||||
should_continue: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'b> ZWordIter<'a, 'b> {
|
||||
fn new(cursor: &'a mut SimpleCursor<'b>) -> Option<Self> {
|
||||
Some(Self { cursor, should_continue: true })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Iterator for ZWordIter<'a, 'b> {
|
||||
type Item = u16;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.should_continue {
|
||||
let next_word = u16::from_be_bytes(*self.cursor.read_const()?);
|
||||
if next_word & 0x8000 != 0 {
|
||||
self.should_continue = false;
|
||||
}
|
||||
Some(next_word)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let alphabet_table = if alphabet_table_addr == 0 { None } else { Some(memory.split_at(alphabet_table_addr).1) };
|
||||
|
||||
let zwords = ZWordIter::new(zchars)?;
|
||||
let zchars = zwords
|
||||
.flat_map(|word| [
|
||||
(word >> 10) & 0x1f,
|
||||
(word >> 5) & 0x1f,
|
||||
word & 0x1f
|
||||
])
|
||||
.map(|word| ZChar(word as u8))
|
||||
.peekable();
|
||||
Some(ZsciiSequence {
|
||||
zchars,
|
||||
subseq: None,
|
||||
alphabet_number: 0,
|
||||
alphabet_table,
|
||||
}.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn decode_hello_world() {
|
||||
// Encoding steps:
|
||||
//
|
||||
// Hello World
|
||||
//
|
||||
// A1: 0d
|
||||
// A0: 0a 11 11 14
|
||||
// 00
|
||||
// A1: 1c
|
||||
// A0: 14 17 11 09
|
||||
//
|
||||
// 04 0d 0a
|
||||
// 11 11 14
|
||||
// 00 04 1c
|
||||
// 14 17 11
|
||||
// 09 04 04
|
||||
//
|
||||
// 00100 01101 01010
|
||||
// 10001 10001 10100
|
||||
// 00000 00100 11100
|
||||
// 10100 10111 10001
|
||||
// 01001 00100 00100
|
||||
//
|
||||
// 0 00100 01101 01010
|
||||
// 0 10001 10001 10100
|
||||
// 0 00000 00100 11100
|
||||
// 0 10100 10111 10001
|
||||
// 1 01001 00100 00100
|
||||
//
|
||||
// 00010001 10101010
|
||||
// 01000110 00110100
|
||||
// 00000000 10011100
|
||||
// 01010010 11110001
|
||||
// 10100100 10000100
|
||||
//
|
||||
// 11 AA
|
||||
// 46 34
|
||||
// 00 9C
|
||||
// 52 F1
|
||||
// A4 84
|
||||
|
||||
// add a couple extra chars to ensure it leaves the cursor in the proper location
|
||||
let memory_buf = &[
|
||||
0x11,
|
||||
0xaa,
|
||||
0x46,
|
||||
0x34,
|
||||
0x00,
|
||||
0x9c,
|
||||
0x52,
|
||||
0xf1,
|
||||
0xa4,
|
||||
0x84,
|
||||
0x11,
|
||||
0xaa
|
||||
];
|
||||
let mut cur = SimpleCursor::new(memory_buf);
|
||||
let out = decode_zchars(&mut cur, 0, 0, memory_buf);
|
||||
assert_eq!(cur.read_const(), Some(&[0x11u8]));
|
||||
assert_eq!(out, Some(zscii_from_bytes(b"Hello World")));
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn decode_zchars_old_2(
|
||||
// zchars: &[u8],
|
||||
// alphabet_table_addr: usize,
|
||||
// abbreviations_table_addr: usize,
|
||||
// memory: &[u8],
|
||||
// ) -> Option<(ZsciiString, usize)> {
|
||||
// fn cut_string(zchars: &[u8]) -> Option<Vec<u16>> {
|
||||
// let mut out = Vec::new();
|
||||
// for word in zchars.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])) {
|
||||
// out.push(word);
|
||||
// if 0x8000 & word != 0 {
|
||||
// return Some(out);
|
||||
// }
|
||||
// }
|
||||
// None
|
||||
// }
|
||||
// fn get_from_alphabet(alphabet_number: usize, ZChar(codepoint): ZChar, alphabet_table: Option<&[u8]>) -> ZsciiChar {
|
||||
|
||||
// ZsciiChar(
|
||||
// alphabet_table
|
||||
// // ~'s indicate invalid characters (reserved values A2:6 and A2:7)
|
||||
// .unwrap_or_else(|| br#"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~~0123456789.,!?_#'"/\-:()"#)
|
||||
// [26 * alphabet_number + codepoint as usize - 6]
|
||||
// )
|
||||
// }
|
||||
// struct ZsciiSequence<'a, T>
|
||||
// where T: Iterator<Item = ZChar> {
|
||||
// zchars: std::iter::Peekable<T>,
|
||||
// subseq: Option<Box<ZsciiSequence<'a, T>>>,
|
||||
// alphabet_number: usize,
|
||||
// alphabet_table: Option<&'a [u8]>
|
||||
// }
|
||||
// impl<'a, T> Iterator for ZsciiSequence<'a, T>
|
||||
// where T: Iterator<Item = ZChar> {
|
||||
// type Item = ZsciiChar;
|
||||
|
||||
// fn next(&mut self) -> Option<Self::Item> {
|
||||
// match self.subseq.as_mut() {
|
||||
// Some(subseq) => {
|
||||
// let out = subseq.next();
|
||||
// if subseq.next().is_none() { self.subseq = None; }
|
||||
// out
|
||||
// },
|
||||
// None => {
|
||||
// let char = self.zchars.next()?;
|
||||
// match char {
|
||||
// ZChar(0) => Some(ZsciiChar(32)),
|
||||
// ZChar(1..=3) => {
|
||||
// self.subseq = Some(Box::new(ZsciiSequence {
|
||||
// zchars: todo!("abbreviations"),
|
||||
// subseq: None,
|
||||
// alphabet_number: self.alphabet_number,
|
||||
// alphabet_table: self.alphabet_table
|
||||
// }));
|
||||
// self.next()
|
||||
// },
|
||||
// ZChar(4..=5) => {
|
||||
// if char == ZChar(5) && self.zchars.peek() == Some(&ZChar(6)) {
|
||||
// Some(ZsciiChar(13))
|
||||
// } else if char == ZChar(5) && self.zchars.peek() == Some(&ZChar(7)) {
|
||||
// let _ = self.zchars.next()?;
|
||||
// let ZChar(z0) = self.zchars.next()?;
|
||||
// let ZChar(z1) = self.zchars.next()?;
|
||||
// Some(ZsciiChar((z0 << 5) | z1))
|
||||
// } else {
|
||||
// self.alphabet_number = char.0 as usize - 3;
|
||||
// let out = self.next();
|
||||
// self.alphabet_number = 0;
|
||||
// out
|
||||
// }
|
||||
// },
|
||||
// ZChar(_) => Some(get_from_alphabet(self.alphabet_number, char, self.alphabet_table))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// let alphabet_table = if alphabet_table_addr == 0 { None } else { Some(memory.split_at(alphabet_table_addr).1) };
|
||||
// let zwords = cut_string(zchars)?;
|
||||
// let consumed_length = zwords.len() * 2;
|
||||
// let zchars = zwords.iter()
|
||||
// .flat_map(|word| [
|
||||
// (word >> 10) & 0x1f,
|
||||
// (word >> 5) & 0x1f,
|
||||
// word & 0x1f
|
||||
// ])
|
||||
// .map(|word| ZChar(word as u8))
|
||||
// .peekable();
|
||||
// Some((ZsciiSequence {
|
||||
// zchars,
|
||||
// subseq: None,
|
||||
// alphabet_number: 0,
|
||||
// alphabet_table,
|
||||
// }.collect(), consumed_length))
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// pub fn decode_zchars_old(
|
||||
// zchars: &[u8],
|
||||
// alphabet_table: Option<&[u8]>,
|
||||
// abbreviations_table: &[u8],
|
||||
// ) -> Option<(ZsciiString, usize)> {
|
||||
// fn cut_string(zchars: &[u8]) -> Option<Vec<u16>> {
|
||||
// let mut out = Vec::new();
|
||||
// for word in zchars.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])) {
|
||||
// out.push(word);
|
||||
// if 0x8000 & word != 0 {
|
||||
// return Some(out);
|
||||
// }
|
||||
// }
|
||||
// None
|
||||
// }
|
||||
|
||||
// /// requires: alphabet_num < 3, 5 < char_idx < 32, !(alphabet_num = 2 AND char_idx = 6)
|
||||
// fn index_alphabet(
|
||||
// alphabet_table: Option<&[u8]>,
|
||||
// alphabet_number: usize,
|
||||
// ZChar(char_idx): ZChar,
|
||||
// zchars: &mut impl Iterator<Item = ZChar>,
|
||||
// ) -> Option<ZsciiChar> {
|
||||
// let default_alphabet: [[ZsciiChar; 26]; 3] = [
|
||||
// b"abcdefghijklmnopqrstuvwxyz".map(ZsciiChar),
|
||||
// b"ABCDEFGHIJKLMNOPQRSTUVWXYZ".map(ZsciiChar),
|
||||
// br#" 0123456789.,!?_#'"/\-:()"#.map(ZsciiChar)
|
||||
// ];
|
||||
// if alphabet_number == 2 && char_idx == 7 {
|
||||
// // newline
|
||||
// Some(ZsciiChar(13))
|
||||
// } else if alphabet_number == 2 && char_idx == 6 {
|
||||
// // direct zscii
|
||||
// let ZChar(next_0) = zchars.next()?;
|
||||
// let ZChar(next_1) = zchars.next()?;
|
||||
// Some(ZsciiChar((next_0 << 5) | next_1))
|
||||
// } else if let Some(alphabet_table) = alphabet_table {
|
||||
// // custom alphabet table
|
||||
// Some(ZsciiChar(alphabet_table[alphabet_number * 26 + char_idx as usize - 6]))
|
||||
// } else {
|
||||
// // default alphabet table
|
||||
// Some(default_alphabet[alphabet_number][char_idx as usize - 6])
|
||||
// }
|
||||
// }
|
||||
|
||||
// let zwords = cut_string(zchars)?;
|
||||
// let consumed_length = zwords.len() * 2;
|
||||
// let mut zchars = zwords.iter()
|
||||
// .flat_map(|word| [
|
||||
// (word >> 10) & 0x1f,
|
||||
// (word >> 5) & 0x1f,
|
||||
// word & 0x1f
|
||||
// ])
|
||||
// .map(|word| ZChar(word as u8))
|
||||
// .peekable();
|
||||
|
||||
// let mut out = Vec::new();
|
||||
// let mut current_alphabet = 0;
|
||||
// while let Some(char) = zchars.next() {
|
||||
// match char {
|
||||
// ZChar(0) => out.push(ZsciiChar(32)),
|
||||
// ZChar(1..=3) => todo!("abbreviations"),
|
||||
// ZChar(4..=5) => if zchars.peek().is_some_and(|&ZChar(n)| n > 5) {
|
||||
// if let Some(zc) = index_alphabet(alphabet_table, char.0 as usize - 3, zchars.next().unwrap(), &mut zchars) { out.push(zc) }
|
||||
// },
|
||||
// ZChar(_) => if let Some(zc) = index_alphabet(alphabet_table, 0, char, &mut zchars) {
|
||||
// out.push(zc)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Some((out, consumed_length))
|
||||
// }
|
@ -1,40 +1,11 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod encoding;
|
||||
mod utils;
|
||||
mod state;
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
// see §11 (page 61) of the spec
|
||||
#[repr(usize)]
|
||||
enum HeaderFields {
|
||||
Version = 0x0,
|
||||
Flags1,
|
||||
HighMemoryStartAddr = 0x4,
|
||||
PCInitValAddr = 0x6,
|
||||
DictionaryAddr = 0x8,
|
||||
ObjectTableAddr = 0xA,
|
||||
GlobalsTableAddr = 0xC,
|
||||
StaticMemoryStartAddr = 0xE,
|
||||
Flags2 = 0x10,
|
||||
AbbreviationsTableaddr = 0x18,
|
||||
FileLength = 0x1A, // divided by a constant; see §11.1.6
|
||||
FileChecksum = 0x1C,
|
||||
InterpreterNumber = 0x1E,
|
||||
InterpreterVersion = 0x1F,
|
||||
ScreenHeightChars = 0x20,
|
||||
ScreenWidthChars = 0x21,
|
||||
ScreenHeightUnits = 0x22,
|
||||
ScreenWidthUnits = 0x24,
|
||||
FontWidthUnits = 0x26,
|
||||
FontHeightUnits = 0x27,
|
||||
BackgroundColor = 0x2C,
|
||||
ForegroundColor = 0x2D,
|
||||
TerminatingCharsTableAddr = 0x2E,
|
||||
RevisionNumber = 0x32,
|
||||
AlphabetTableAddr = 0x34,
|
||||
HeaderExtensionTableAddr = 0x36,
|
||||
}
|
||||
|
||||
// note: this is a table of words, unlike the regular header which is all bytes
|
||||
#[repr(usize)]
|
||||
enum ExtensionTableFields {
|
@ -0,0 +1,27 @@
|
||||
pub struct Z5 {
|
||||
memory: [u8; 128 * 1024],
|
||||
pc: usize,
|
||||
call_stack: Vec<StackFrame>,
|
||||
}
|
||||
|
||||
pub enum ReturnTarget {
|
||||
ReturnIllegal,
|
||||
Discard,
|
||||
/// note that writing to a local variable targets the local on the stack frame
|
||||
/// being returned *to*, not the stack frame being returned *from*, as the latter
|
||||
/// is discarded during the return
|
||||
WriteVariable(u8),
|
||||
}
|
||||
|
||||
pub struct StackFrame {
|
||||
stack: Vec<u16>,
|
||||
// length must be no greater than 15
|
||||
locals: Vec<u16>,
|
||||
return_address: usize,
|
||||
}
|
||||
|
||||
impl Z5 {
|
||||
pub fn run_instruction(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
pub mod simplecursor;
|
@ -0,0 +1,161 @@
|
||||
#[derive(Clone)]
|
||||
pub struct SimpleCursor<'a> {
|
||||
buffer: &'a [u8],
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a> SimpleCursor<'a> {
|
||||
pub fn new(buffer: &'a [u8]) -> Self {
|
||||
Self { buffer, index: 0 }
|
||||
}
|
||||
|
||||
pub fn new_with_index(buffer: &'a [u8], index: usize) -> Self {
|
||||
Self { buffer, index }
|
||||
}
|
||||
|
||||
pub fn buffer_length(&self) -> usize { self.buffer.len() }
|
||||
|
||||
fn remaining_length(&self) -> usize {
|
||||
self.buffer_length() - self.index
|
||||
}
|
||||
|
||||
pub fn read_const<const N: usize>(&mut self) -> Option<&[u8; N]> {
|
||||
if self.remaining_length() >= N {
|
||||
// safety: first chunk is of length N < remaining_length
|
||||
let out = self.buffer[self.index..].first_chunk().unwrap();
|
||||
self.index += N;
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self, n: usize) -> Option<&[u8]> {
|
||||
if self.remaining_length() >= n {
|
||||
let out = &self.buffer[self.index..(self.index + n)];
|
||||
self.index += n;
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_to_end(&mut self) -> &[u8] {
|
||||
let out = &self.buffer[self.index..self.buffer_length()];
|
||||
self.index = self.buffer_length();
|
||||
out
|
||||
}
|
||||
|
||||
pub fn peek_const<const N: usize>(&self) -> Option<&[u8; N]> {
|
||||
if self.remaining_length() >= N {
|
||||
// safety: first chunk is of length N < remaining_length
|
||||
Some(self.buffer[self.index..].first_chunk().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek(&self, n: usize) -> Option<&[u8]> {
|
||||
if self.remaining_length() >= n {
|
||||
Some(&self.buffer[self.index..(self.index + n)])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek_to_end(&self) -> &[u8] {
|
||||
&self.buffer[self.index..self.buffer_length()]
|
||||
}
|
||||
|
||||
pub fn seek(&mut self, index: usize) -> Result<(), ()> {
|
||||
if index <= self.buffer_length() {
|
||||
self.index = index;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn seek_relative(&mut self, distance: isize) -> Result<(), ()> {
|
||||
if -distance > self.index as isize {
|
||||
Err(())
|
||||
} else {
|
||||
self.seek((self.index as isize + distance) as usize)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn at_end(&self) -> bool {
|
||||
self.index == self.buffer_length()
|
||||
}
|
||||
|
||||
pub fn buf(&self) -> &[u8] { self.buffer }
|
||||
|
||||
pub fn index(&self) -> usize { self.index }
|
||||
}
|
||||
|
||||
impl<'a> Iterator for SimpleCursor<'a> {
|
||||
type Item = u8;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.read_const::<1>().map(|s| s[0])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn read_const() {
|
||||
let buf = &[1, 2, 3, 4, 5];
|
||||
let mut c = SimpleCursor::new(buf);
|
||||
assert_eq!(c.read_const::<3>(), Some(&[1, 2, 3]));
|
||||
assert_eq!(c.read_const::<3>(), None);
|
||||
assert_eq!(c.read_const::<2>(), Some(&[4, 5]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_non_const() {
|
||||
let buf = &[1, 2, 3, 4, 5];
|
||||
let mut c = SimpleCursor::new(buf);
|
||||
let Some(a) = c.read(3) else {panic!("reading 3 should succeed")};
|
||||
assert_eq!(a.len(), 3);
|
||||
assert_eq!(a[0], 1);
|
||||
assert_eq!(a[1], 2);
|
||||
assert_eq!(a[2], 3);
|
||||
assert_eq!(c.read(3), None);
|
||||
let Some(a) = c.read(2) else {panic!("reading 2 should succeed")};
|
||||
assert_eq!(a.len(), 2);
|
||||
assert_eq!(a[0], 4);
|
||||
assert_eq!(a[1], 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_to_end() {
|
||||
let buf = &[1, 2, 3, 4, 5];
|
||||
let mut c = SimpleCursor::new(buf);
|
||||
assert_eq!(c.read_const::<2>(), Some(&[1, 2]));
|
||||
let a = c.read_to_end();
|
||||
assert_eq!(a.len(), 3);
|
||||
assert_eq!(a[0], 3);
|
||||
assert_eq!(a[1], 4);
|
||||
assert_eq!(a[2], 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seek() {
|
||||
let buf = &[1, 2, 3, 4, 5];
|
||||
let mut c = SimpleCursor::new(buf);
|
||||
assert_eq!(c.read_const::<3>(), Some(&[1, 2, 3]));
|
||||
c.seek(1).unwrap();
|
||||
assert_eq!(c.read_const::<3>(), Some(&[2, 3, 4]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seek_fail() {
|
||||
let buf = &[1, 2, 3, 4, 5];
|
||||
let mut c = SimpleCursor::new(buf);
|
||||
assert_eq!(c.seek(5), Ok(()));
|
||||
assert_eq!(c.seek(4), Ok(()));
|
||||
assert_eq!(c.seek(6), Err(()));
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "zingprocmacros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = { version = "1.0.49", features = ["diff"] }
|
||||
|
||||
[dependencies]
|
||||
quote = "1.0.36"
|
||||
syn = "2.0.63"
|
@ -0,0 +1,17 @@
|
||||
use zingprocmacros::make_instruction_type;
|
||||
|
||||
struct ZsciiString;
|
||||
|
||||
make_instruction_type!{MyInstruction {
|
||||
1 FirstInst,
|
||||
0b10 SecondInst st,
|
||||
3u8 ThirdInst br txt,
|
||||
12 FourthInst txt,
|
||||
0xC FifthInst br,
|
||||
}}
|
||||
|
||||
fn main() {
|
||||
let one = MyInstruction::FirstInst;
|
||||
let two = MyInstruction::SecondInst { store: 0 };
|
||||
let three = MyInstruction::ThirdInst { branch: 0, text: ZsciiString };
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
use quote::quote;
|
||||
use syn::{braced, parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, token::Brace, Ident, LitInt, Token};
|
||||
|
||||
mod token {
|
||||
syn::custom_keyword!(st);
|
||||
syn::custom_keyword!(br);
|
||||
syn::custom_keyword!(txt);
|
||||
pub type Store = st;
|
||||
pub type Branch = br;
|
||||
pub type Text = txt;
|
||||
}
|
||||
|
||||
struct InstructionEntry {
|
||||
number: LitInt,
|
||||
name: Ident,
|
||||
store: Option<token::Store>,
|
||||
branch: Option<token::Branch>,
|
||||
text: Option<token::Text>,
|
||||
}
|
||||
|
||||
struct InstructionType {
|
||||
type_name: Ident,
|
||||
brace_token: Brace,
|
||||
entries: Punctuated<InstructionEntry, Token![,]>,
|
||||
}
|
||||
|
||||
impl Parse for InstructionEntry {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
Ok(Self {
|
||||
number: input.parse()?,
|
||||
name: input.parse()?,
|
||||
store: input.parse()?,
|
||||
branch: input.parse()?,
|
||||
text: input.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for InstructionType {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let content;
|
||||
Ok(Self {
|
||||
type_name: input.parse()?,
|
||||
brace_token: braced!(content in input),
|
||||
entries: content.parse_terminated(InstructionEntry::parse, Token![,])?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn make_instruction_type(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let instruction_type = parse_macro_input!(tokens as InstructionType);
|
||||
let type_name = instruction_type.type_name;
|
||||
|
||||
let number_values = instruction_type.entries.iter().map(|e| e.number.base10_parse::<u8>().unwrap());
|
||||
// check for duplicates
|
||||
{
|
||||
let mut buckets = [0u8; 256];
|
||||
for n in number_values {
|
||||
let n = n as usize;
|
||||
buckets[n] += 1;
|
||||
if buckets[n] == 2 {panic!("no duplicate numeric entries allowed! duplicated number: {}", n)}
|
||||
}
|
||||
}
|
||||
|
||||
let initial_enum_variants = instruction_type.entries.iter()
|
||||
.map(|InstructionEntry { name, store, branch, text, .. }| {
|
||||
if store.is_none() && branch.is_none() && text.is_none() {
|
||||
quote!{#name}
|
||||
} else {
|
||||
let store = store.map(|_| quote! { store: u8, });
|
||||
let branch = branch.map(|_| quote! { branch: Branch, });
|
||||
let text = text.map(|_| quote! { text: ZsciiString, });
|
||||
quote! { #name { #store #branch #text } }
|
||||
}
|
||||
});
|
||||
|
||||
let variant_names = instruction_type.entries.iter().map(|e| &e.name);
|
||||
let has_store_branches = instruction_type.entries.iter().map(|e| {
|
||||
let has_store = e.store.is_some();
|
||||
let number = &e.number;
|
||||
quote!{ #number => #has_store }
|
||||
});
|
||||
let has_branch_branches = instruction_type.entries.iter().map(|e| {
|
||||
let has_branch = e.branch.is_some();
|
||||
let number = &e.number;
|
||||
quote!{ #number => #has_branch }
|
||||
});
|
||||
let has_text_branches = instruction_type.entries.iter().map(|e| {
|
||||
let has_text = e.text.is_some();
|
||||
let number = &e.number;
|
||||
quote!{ #number => #has_text }
|
||||
});
|
||||
|
||||
let constructor_function_branches = instruction_type.entries.iter().map(|InstructionEntry { number, name, store, branch, text }| {
|
||||
if store.is_none() && branch.is_none() && text.is_none() {
|
||||
quote!{ #number => ::std::result::Result::Ok(Self::#name) }
|
||||
} else {
|
||||
let store = store.map(|_| quote! { store: store.ok_or(InstructionDecodeError::MissingStore)?, });
|
||||
let branch = branch.map(|_| quote! { branch: branch.ok_or(InstructionDecodeError::MissingBranch)?, });
|
||||
let text = text.map(|_| quote! { text: text.ok_or(InstructionDecodeError::MissingText)?, });
|
||||
quote! { #number => ::std::result::Result::Ok(Self::#name {#store #branch #text}) }
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
pub enum #type_name { #(#initial_enum_variants),* }
|
||||
|
||||
impl Opcode for #type_name {
|
||||
fn has_store(inst_no: u8) -> bool {
|
||||
match inst_no {
|
||||
#(#has_store_branches,)*
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
fn has_branch(inst_no: u8) -> bool {
|
||||
match inst_no {
|
||||
#(#has_branch_branches,)*
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
fn has_text(inst_no: u8) -> bool {
|
||||
match inst_no {
|
||||
#(#has_text_branches,)*
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
fn from_number(inst_no: u8, store: Option<u8>, branch: Option<Branch>, text: Option<ZsciiString>) -> Result<Self, InstructionDecodeError> {
|
||||
match inst_no {
|
||||
#(#constructor_function_branches,)*
|
||||
_ => Err(InstructionDecodeError::InvalidOpcodeNum(inst_no))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn builds() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.pass("tests/*.rs");
|
||||
// t.compile_fail("negative-tests/*.rs");
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
use zingprocmacros::make_instruction_type;
|
||||
|
||||
struct ZsciiString;
|
||||
|
||||
make_instruction_type!{MyInstruction {
|
||||
1 FirstInst,
|
||||
0b10 SecondInst st,
|
||||
3u8 ThirdInst br txt,
|
||||
}}
|
||||
|
||||
fn main() {
|
||||
let one = MyInstruction::FirstInst;
|
||||
let two = MyInstruction::SecondInst { store: 0 };
|
||||
let three = MyInstruction::ThirdInst { branch: 0, text: ZsciiString };
|
||||
}
|
Loading…
Reference in New Issue