diff --git a/Cargo.lock b/Cargo.lock index a4b2e16..e2a6b36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,294 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "dissimilar" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "proc-macro2" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.201" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.201" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "2.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "trybuild" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a5f13f11071020bb12de7a16b925d2d58636175c20c11dc5f96cb64bb6c9b3" +dependencies = [ + "dissimilar", + "glob", + "serde", + "serde_derive", + "serde_json", + "termcolor", + "toml", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +dependencies = [ + "memchr", +] + [[package]] name = "zing" version = "0.1.0" +dependencies = [ + "zingprocmacros", +] + +[[package]] +name = "zingprocmacros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", + "trybuild", +] diff --git a/Cargo.toml b/Cargo.toml index 4db0a55..d63f3a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", +] diff --git a/src/encoding.rs b/src/encoding.rs deleted file mode 100644 index 3b9debd..0000000 --- a/src/encoding.rs +++ /dev/null @@ -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; - -/// 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> { - 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, - ) -> Option { - 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) { - -} diff --git a/zing/Cargo.toml b/zing/Cargo.toml new file mode 100644 index 0000000..9f6c1c8 --- /dev/null +++ b/zing/Cargo.toml @@ -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" } diff --git a/zing/src/encoding.rs b/zing/src/encoding.rs new file mode 100644 index 0000000..087c446 --- /dev/null +++ b/zing/src/encoding.rs @@ -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; +} + diff --git a/zing/src/encoding/instruction.rs b/zing/src/encoding/instruction.rs new file mode 100644 index 0000000..eb060d6 --- /dev/null +++ b/zing/src/encoding/instruction.rs @@ -0,0 +1,204 @@ +use zingprocmacros::make_instruction_type; +use crate::encoding::text::ZsciiString; + +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, + opcode: VariableVarOpOpcode + }, + ExtendedVarOp { + operands: Vec, + opcode: ExtendedVarOpOpcode, + }, +} + +pub enum VariableOperands { + TwoOp(Operand, Operand), + VarOp(Vec), +} + +pub enum Operand { + SmallConstant(u8), + LargeConstant(u16), + Variable(u8), +} + +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!(Long2OpOpcode { + 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 Variable2OpOpcode = Long2OpOpcode; + +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), +} + +/// Returns: Some((instruction, instruction_length)) if instruction is valid, None otherwise +pub fn decode_instruction(memory: &[u8], address: usize) -> Result<(Instruction, usize), InstructionDecodeError> { + // TODO + fn decode_short(memory: &[u8], address: usize) -> Result<(Instruction, usize), InstructionDecodeError> { + let mut cursor = address; + let base = memory[cursor]; + cursor += 1; + let opcode_num = base & 0x0f; + let has_operand = base & 0x30 != 0x30; + if has_operand { + todo!("get operand and define store/branch/text if necessary"); + // let instruction = Short1OpOpcode::from_number(opcode_num, store, branch, text).ok_or(InstructionDecodeError::InvalidOpcodeNum(opcode_num))?; + todo!() + } else { + todo!() + } + } + + // TODO + fn decode_long(memory: &[u8], address: usize) -> Result<(Instruction, usize), InstructionDecodeError> { + todo!() + } + + // TODO + fn decode_extended(memory: &[u8], address: usize) -> Result<(Instruction, usize), InstructionDecodeError> { + todo!() + } + + // TODO + fn decode_variable(memory: &[u8], address: usize) -> Result<(Instruction, usize), InstructionDecodeError> { + todo!() + } + + if memory[address] == 190 { + decode_extended(memory, address) + } else if memory[address] & 0xC0 == 0xC0 { + decode_variable(memory, address) + } else if memory[address] & 0xC0 == 0x80 { + decode_short(memory, address) + } else { + decode_long(memory, address) + } +} + + diff --git a/zing/src/encoding/text.rs b/zing/src/encoding/text.rs new file mode 100644 index 0000000..6cf45ce --- /dev/null +++ b/zing/src/encoding/text.rs @@ -0,0 +1,184 @@ +/// 5 bits +#[derive(Clone, Copy, PartialEq)] +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; + +/// 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_addr: usize, + abbreviations_table_addr: usize, + memory: &[u8], +) -> Option<(ZsciiString, usize)> { + fn cut_string(zchars: &[u8]) -> Option> { + 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 { + zchars: std::iter::Peekable, + subseq: Option>>, + alphabet_number: usize, + alphabet_table: Option<&'a [u8]> + } + impl<'a, T> Iterator for ZsciiSequence<'a, T> + where T: Iterator { + type Item = ZsciiChar; + + fn next(&mut self) -> Option { + 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!(), + 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> { +// 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, +// ) -> Option { +// 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)) +// } diff --git a/src/main.rs b/zing/src/main.rs similarity index 53% rename from src/main.rs rename to zing/src/main.rs index 242897e..98ef43c 100644 --- a/src/main.rs +++ b/zing/src/main.rs @@ -1,40 +1,10 @@ #![allow(dead_code)] mod encoding; +mod utils; 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 { diff --git a/zing/src/utils.rs b/zing/src/utils.rs new file mode 100644 index 0000000..5454bcc --- /dev/null +++ b/zing/src/utils.rs @@ -0,0 +1 @@ +pub mod simplecursor; diff --git a/zing/src/utils/simplecursor.rs b/zing/src/utils/simplecursor.rs new file mode 100644 index 0000000..8b18f10 --- /dev/null +++ b/zing/src/utils/simplecursor.rs @@ -0,0 +1,149 @@ +#[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 buffer_length(&self) -> usize { self.buffer.len() } + + fn remaining_length(&self) -> usize { + self.buffer_length() - self.index + } + + pub fn read_const(&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(&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 at_end(&self) -> bool { + self.index == self.buffer_length() + } + + pub fn buf(&self) -> &[u8] { + self.buffer + } +} + +impl<'a> Iterator for SimpleCursor<'a> { + type Item = u8; + + fn next(&mut self) -> Option { + 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(())); + } +} diff --git a/z-spec10.pdf b/zing/z-spec10.pdf similarity index 100% rename from z-spec10.pdf rename to zing/z-spec10.pdf diff --git a/zingprocmacros/Cargo.toml b/zingprocmacros/Cargo.toml new file mode 100644 index 0000000..a09ab0b --- /dev/null +++ b/zingprocmacros/Cargo.toml @@ -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" diff --git a/zingprocmacros/negative-tests/01-repeated-digits-fail.rs b/zingprocmacros/negative-tests/01-repeated-digits-fail.rs new file mode 100644 index 0000000..7f4fc15 --- /dev/null +++ b/zingprocmacros/negative-tests/01-repeated-digits-fail.rs @@ -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 }; +} diff --git a/zingprocmacros/src/lib.rs b/zingprocmacros/src/lib.rs new file mode 100644 index 0000000..1577093 --- /dev/null +++ b/zingprocmacros/src/lib.rs @@ -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, + branch: Option, + text: Option, +} + +struct InstructionType { + type_name: Ident, + brace_token: Brace, + entries: Punctuated, +} + +impl Parse for InstructionEntry { + fn parse(input: ParseStream) -> syn::Result { + 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 { + 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::().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: u16, }); + 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::option::Option::Some(Self::#name) } + } else { + let store = store.map(|_| quote! { store: store?, }); + let branch = branch.map(|_| quote! { branch: branch?, }); + let text = text.map(|_| quote! { text: text?, }); + quote! { #number => ::std::option::Option::Some(Self::#name {#store #branch #text}) } + } + }); + + quote! { + pub enum #type_name { #(#initial_enum_variants),* } + + impl #type_name { + pub fn has_store(inst_no: u8) -> bool { + match inst_no { + #(#has_store_branches,)* + _ => false + } + } + + pub fn has_branch(inst_no: u8) -> bool { + match inst_no { + #(#has_branch_branches,)* + _ => false + } + } + + pub fn has_text(inst_no: u8) -> bool { + match inst_no { + #(#has_text_branches,)* + _ => false + } + } + + pub fn from_number(inst_no: u8, store: Option, branch: Option, text: Option) -> Option { + match inst_no { + #(#constructor_function_branches,)* + _ => None + } + } + } + }.into() +} + +#[cfg(test)] +mod tests { + #[test] + fn builds() { + let t = trybuild::TestCases::new(); + t.pass("tests/*.rs"); + // t.compile_fail("negative-tests/*.rs"); + + } +} diff --git a/zingprocmacros/tests/01-constructors-pass.rs b/zingprocmacros/tests/01-constructors-pass.rs new file mode 100644 index 0000000..389516f --- /dev/null +++ b/zingprocmacros/tests/01-constructors-pass.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 }; +}