├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── glulxtoc ├── Cargo.toml ├── README.md ├── glulxtoc-logo.png ├── glulxtoc-logo.svg └── src │ ├── main.rs │ └── output │ ├── files.rs │ ├── functions_common.rs │ ├── functions_safe.rs │ ├── functions_unsafe.rs │ ├── image.rs │ ├── mod.rs │ └── templates │ ├── CMakeLists.txt │ ├── LICENSE │ ├── glulxtoc.h │ ├── runtime.c │ └── unixstrt.c ├── if-decompiler ├── Cargo.toml ├── README.md └── src │ ├── glulx │ ├── disassembler.rs │ ├── mod.rs │ └── opcodes.rs │ └── lib.rs ├── relooper ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ └── tests │ ├── glulxercise.rs │ ├── inform6lib.rs │ ├── inform7.rs │ └── mod.rs ├── tests ├── advent.ulx ├── advent.ulx.regtest ├── glulxercise.ulx ├── glulxercise.ulx.gameinfo.dbg ├── glulxercise.ulx.regtest ├── prepare.sh ├── runalltests.sh └── runtest.sh └── tools └── reduce-debug-xml.sh /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-and-test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | submodules: true 12 | - uses: actions-rs/toolchain@v1 13 | with: 14 | toolchain: stable 15 | - run: ./tests/prepare.sh cheapglk regtest remglk 16 | - run: cargo build 17 | - run: cargo test --package relooper 18 | - run: ./tests/runtest.sh -f tests/glulxercise.ulx -d 19 | - run: ./tests/runtest.sh -f tests/glulxercise.ulx -u 27057 20 | - run: ./tests/runtest.sh -f tests/advent.ulx 21 | - run: ./tests/runtest.sh -f tests/advent.ulx -r -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | tests/*glk 3 | tests/regtest.py 4 | tests/*.decompiled 5 | tests/*.disassembled -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "glulxe"] 2 | path = glulxtoc/src/upstream/glulxe 3 | url = https://github.com/erkyrath/glulxe.git -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "glulxtoc", 4 | "if-decompiler", 5 | "relooper", 6 | ] 7 | 8 | [profile.release] 9 | lto = true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2021, Dannii Willis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Glulxtoc and IF-Decompiler 2 | ========================== 3 | 4 | In this repository you will find: 5 | 6 | [Glulxtoc](./glulxtoc) 7 | ---------------------- 8 | 9 | [![Glulxtoc logo](./glulxtoc/glulxtoc-logo.png)](./glulxtoc) 10 | 11 | Glulxtoc will decompile your Glulx storyfile into C code which you can then compile against any Glk library. 12 | 13 | [Glulxtoc installation and usage instructions](./glulxtoc/README.md). 14 | 15 | [IF-Decompiler](./if-decompiler) 16 | -------------------------------- 17 | 18 | Decompile various interactive fiction formats. 19 | 20 | [Relooper](./relooper) 21 | ---------------------- 22 | 23 | Relooper turns unstructured branches and jumps into structured blocks. -------------------------------------------------------------------------------- /glulxtoc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "glulxtoc" 3 | version = "0.1.0" 4 | authors = ["Dannii Willis "] 5 | edition = "2018" 6 | description = "Decompile Glulx storyfiles into C code" 7 | homepage = "https://github.com/curiousdannii/if-decompiler" 8 | license = "MIT" 9 | repository = "https://github.com/curiousdannii/if-decompiler" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | bytes = "1.0.1" 15 | dyn-fmt = "0.3.0" 16 | fnv = "1.0.7" 17 | if-decompiler = {path = "../if-decompiler", version = "0.1.0"} 18 | quick-xml = "0.22" 19 | relooper = {path = "../relooper", version = "0.1.0"} 20 | structopt = "0.3.13" -------------------------------------------------------------------------------- /glulxtoc/README.md: -------------------------------------------------------------------------------- 1 | Glulxtoc - Glulx to C decompiler 2 | ================================ 3 | 4 | ![Glulxtoc logo](https://raw.githubusercontent.com/curiousdannii/if-decompiler/master/glulxtoc/glulxtoc-logo.png) 5 | 6 | Glulxtoc will decompile your Glulx storyfile into C code which you can then compile against any Glk library. 7 | 8 | To get it, first [install Rust](https://rustup.rs/) and then install glulxtoc with cargo: 9 | 10 | ``` 11 | cargo install glulxtoc 12 | ``` 13 | 14 | Usage 15 | ----- 16 | 17 | ``` 18 | glulxtoc [FLAGS] [OPTIONS] 19 | ``` 20 | 21 | Required option: 22 | 23 | - path to storyfile 24 | 25 | Flags: 26 | 27 | - `-d`, `--disassemble`: Disassembler mode - only disassemble, do not optimise or generate structured code 28 | 29 | Options: 30 | 31 | - `--debug-file`: path to an Inform debug file for the storyfile 32 | - `--out-dir`: Output folder. If not given will make a folder based on the storyfile's name with `.decompiled` added to the end 33 | - `--stack-size`: Stack size in MB (default 8), for the glulxtoc app (not the stack of the Glulx file being decompiled.) Very large storyfiles may cause the glulxtoc app to have a stack overflow, in which case pass this option. 34 | - `--safe-function-overrides`: An array of function addresses to forcibly set as safe, overriding the decompiler's heuristics. Example, `--safe-function-overrides=1234,5678` 35 | - `--unsafe-function-overrides`: An array of function addresses to forcibly set as unsafe, overriding the decompiler's heuristics. 36 | 37 | Compiling the output code 38 | ------------------------- 39 | 40 | Glulxtoc produces several C files and provides a CMake CMakeLists.txt. You must pass in the Glk library's path to CMake. For example: 41 | 42 | ``` 43 | glulxtoc advent.ulx 44 | cd advent.ulx.decompiled 45 | mkdir remglk 46 | cmake -DGlkLibPath=../../remglk . -B remglk 47 | cd remglk 48 | make 49 | ``` 50 | 51 | Limitations 52 | ----------- 53 | 54 | In general Glulxtoc is likely to have problems with any Glulx files that weren't compiled with Inform. 55 | 56 | - No functions in RAM 57 | - Functions and strings can't be interleaved - will stop decoding once the first string is found 58 | - No 1 and 2 byte locals 59 | - Inter-function branches are only supported when you manually set the target function as unsafe 60 | - State changing opcodes (save, restart, etc) within functions called by strings 61 | 62 | Troubleshooting 63 | --------------- 64 | 65 | - If you get an error in the decompilation stage (such as an unknown opcode), try passing in an Inform debug file. If you provide one, consider using the [reduce-debug-xml.sh](https://github.com/curiousdannii/if-decompiler/blob/master/tools/reduce-debug-xml.sh) tool to cut back the debug data to only what Glulxtoc makes use of. This is not required, but will make Glulxtoc run faster. 66 | - If the `make` stage of compilation is very slow, try Clang. GCC has [a bug](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=100393) which makes Glulxtoc's output compile very slowly. 67 | - If it compiles without error, but does not run properly, see if switching Glulxtoc to the disassembler mode (`-d`) fixes things. If it does then that indicates a bug in Glulxtoc's decompilation optimisation code. 68 | 69 | If you do get an error, please post a [bug report](https://github.com/curiousdannii/if-decompiler/issues) with as much detail as you can provide, and ideally with your storyfile. -------------------------------------------------------------------------------- /glulxtoc/glulxtoc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curiousdannii/if-decompiler/e9b0c6886bbb6aff1a4c441fb03f7cb387cbb494/glulxtoc/glulxtoc-logo.png -------------------------------------------------------------------------------- /glulxtoc/glulxtoc-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 58 | 69 | 80 | glulxtoc 91 | IF-Decompiler 102 | 103 | 104 | -------------------------------------------------------------------------------- /glulxtoc/src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | glulxtoc - Decompile a Glulx file into C code 4 | ============================================= 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use std::collections::BTreeMap; 13 | use std::env; 14 | use std::fs::File; 15 | use std::io; 16 | use std::io::{BufReader, Cursor, Write}; 17 | use std::path::PathBuf; 18 | use std::time::Instant; 19 | use std::thread; 20 | 21 | use bytes::Buf; 22 | use quick_xml; 23 | use structopt::StructOpt; 24 | 25 | use if_decompiler; 26 | use if_decompiler::DebugFunctionData; 27 | 28 | mod output; 29 | 30 | #[derive(StructOpt)] 31 | #[structopt(name = "glulxtoc", about = "Decompile a Glulx file into C code")] 32 | struct Cli { 33 | /// The path of the Glulxe storyfile 34 | #[structopt(parse(from_os_str))] 35 | path: PathBuf, 36 | 37 | /// Output folder 38 | #[structopt(long, parse(from_os_str))] 39 | out_dir: Option, 40 | 41 | /// Stack size (MB) (for the glulxtoc app, not the stack of the Glulx file being decompiled) 42 | #[structopt(short, long, default_value = "8")] 43 | stack_size: usize, 44 | 45 | /// Inform debug file 46 | #[structopt(long, parse(from_os_str))] 47 | debug_file: Option, 48 | 49 | /// Disassembler mode - only disassemble, do not optimise or generate structured code 50 | #[structopt(short, long)] 51 | disassemble: bool, 52 | 53 | /// Safe function overrides 54 | #[structopt(long, use_delimiter = true)] 55 | safe_function_overrides: Option>, 56 | 57 | /// Unsafe function overrides 58 | #[structopt(long, use_delimiter = true)] 59 | unsafe_function_overrides: Option>, 60 | } 61 | 62 | fn main() -> Result<(), Box> { 63 | // Process arguments 64 | let args: Cli = Cli::from_args(); 65 | 66 | let child = thread::Builder::new() 67 | .name("run".into()) 68 | .stack_size(args.stack_size * 1024 * 1024) 69 | .spawn(move || -> Result<(), Box> { run(args)?; Ok(()) }) 70 | .unwrap(); 71 | 72 | child.join().unwrap()?; 73 | 74 | Ok(()) 75 | } 76 | 77 | fn run(args: Cli) -> Result<(), Box> { 78 | // Start processing args 79 | let mut storyfile_path = env::current_dir()?; 80 | storyfile_path.push(args.path); 81 | let name = storyfile_path.file_stem().expect("storyfile should not be relative").to_str().unwrap().to_string(); 82 | 83 | let out_dir = match args.out_dir { 84 | Some(path) => path, 85 | None => { 86 | let mut path = storyfile_path.clone(); 87 | let mut name = path.file_name().unwrap().to_os_string(); 88 | name.push(if args.disassemble { ".disassembled" } else { ".decompiled" }); 89 | path.pop(); 90 | path.push(name); 91 | path 92 | } 93 | }; 94 | 95 | // Read the storyfile 96 | println!("Starting to decompile {:?}", storyfile_path); 97 | let start = Instant::now(); 98 | let data = std::fs::read(storyfile_path)?; 99 | let data_length = data.len(); 100 | 101 | // Start parsing the file 102 | fn get_file_header(data: &[u8]) -> (u32, u32) { 103 | let mut cursor = Cursor::new(data); 104 | let magic = cursor.get_u32(); 105 | cursor.set_position(8); 106 | let iff_type = cursor.get_u32(); 107 | (magic, iff_type) 108 | } 109 | let (magic, iff_type) = get_file_header(&data); 110 | 111 | // Check for a blorb 112 | let image = if magic == 0x464F524D /* FORM */ && iff_type == 0x49465253 /* IFRS */ { 113 | parse_blorb(&data) 114 | } 115 | // A bare Glulx file 116 | else if magic == 0x476C756C /* Glul */ { 117 | Some(&*data) 118 | } 119 | else { 120 | panic!("Unrecognised file format"); 121 | }; 122 | 123 | // Read the debug file if specified 124 | let debug_function_data = match args.debug_file { 125 | Some(path) => { 126 | print!("Parsing the debug file..."); 127 | io::stdout().flush().unwrap(); 128 | let start_parse_debug_file = Instant::now(); 129 | let file = File::open(path)?; 130 | let result = Some(parse_debug_file(BufReader::new(file)).expect("Error parsing XML")); 131 | println!(" completed in {:?}", start_parse_debug_file.elapsed()); 132 | result 133 | }, 134 | None => None, 135 | }; 136 | 137 | // Decompile the storyfile 138 | print!("Disassembling the storyfile..."); 139 | io::stdout().flush().unwrap(); 140 | let start_disassemble = Instant::now(); 141 | let mut decompiler = if_decompiler::glulx::GlulxState::new(debug_function_data, args.safe_function_overrides, true, args.unsafe_function_overrides); 142 | decompiler.decompile_rom(image.unwrap()); 143 | let duration = start_disassemble.elapsed(); 144 | println!(" completed in {:?}", duration); 145 | 146 | // Output the C files 147 | let mut output = output::GlulxOutput::new(args.disassemble, data_length as u32, name, out_dir, decompiler); 148 | output.output(&data)?; 149 | 150 | let duration = start.elapsed(); 151 | println!("Total decompilation time: {:?}", duration); 152 | 153 | Ok(()) 154 | } 155 | 156 | // Parse a blorb file 157 | // TODO: parse debug data from blorb 158 | fn parse_blorb<'a>(data: &'a [u8]) -> Option<&'a [u8]> { 159 | let length = data.len() as u64; 160 | let mut cursor = Cursor::new(data); 161 | cursor.set_position(12); 162 | let mut glulx_chunk = None; 163 | while cursor.position() < length { 164 | let chunk_type = cursor.get_u32(); 165 | let chunk_length = cursor.get_u32(); 166 | let chunk_end = cursor.position()as usize + chunk_length as usize; 167 | match chunk_type { 168 | 0x474C554C /* GLUL */ => { 169 | glulx_chunk = Some(&data[cursor.position() as usize..chunk_end]); 170 | }, 171 | _ => {}, 172 | } 173 | cursor.set_position(chunk_end as u64); 174 | } 175 | if glulx_chunk.is_none() { 176 | panic!("Blorb file does not have a GLUL chunk"); 177 | } 178 | glulx_chunk 179 | } 180 | 181 | // Parse an Inform debug file 182 | fn parse_debug_file(str: BufReader) -> quick_xml::Result> { 183 | use quick_xml::events::Event; 184 | let mut reader = quick_xml::Reader::from_reader(str); 185 | reader.trim_text(true); 186 | let mut result = BTreeMap::default(); 187 | let mut buf = Vec::new(); 188 | let mut in_routine = false; 189 | let mut process_text = false; 190 | let mut text = String::new(); 191 | let mut addr = 0; 192 | let mut len = 0; 193 | let mut name = String::new(); 194 | 195 | loop { 196 | match reader.read_event(&mut buf) { 197 | Ok(Event::Start(ref e)) => { 198 | match e.name() { 199 | b"byte-count" | b"identifier" | b"value" => { 200 | process_text = true; 201 | text.clear(); 202 | }, 203 | b"routine" => { 204 | in_routine = true; 205 | }, 206 | _ => {}, 207 | }; 208 | }, 209 | Ok(Event::Text(e)) => { 210 | if in_routine && process_text { 211 | text.push_str(&e.unescape_and_decode(&reader)?); 212 | } 213 | }, 214 | Ok(Event::End(ref e)) => { 215 | match e.name() { 216 | b"byte-count" => { 217 | if in_routine { 218 | len = text.parse::().unwrap(); 219 | } 220 | }, 221 | b"identifier" => { 222 | if in_routine && name == "" { 223 | name = text.clone(); 224 | } 225 | }, 226 | b"routine" => { 227 | in_routine = false; 228 | result.insert(addr, DebugFunctionData { 229 | addr, 230 | len, 231 | name: name.clone(), 232 | }); 233 | name.clear(); 234 | }, 235 | b"value" => { 236 | if in_routine { 237 | addr = text.parse::().unwrap(); 238 | } 239 | }, 240 | _ => {}, 241 | }; 242 | process_text = false; 243 | }, 244 | Ok(Event::Eof) => break, 245 | Err(e) => panic!("XML error in debug file at position {}: {:?}", reader.buffer_position(), e), 246 | _ => (), 247 | }; 248 | buf.clear(); 249 | }; 250 | 251 | Ok(result) 252 | } -------------------------------------------------------------------------------- /glulxtoc/src/output/files.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Create files 4 | ============ 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use super::*; 13 | use std::time::Instant; 14 | 15 | impl GlulxOutput { 16 | pub fn output_from_templates(&self, data: &[u8]) -> std::io::Result<()> { 17 | let start = Instant::now(); 18 | 19 | // Output the image 20 | let mut output_path = self.out_dir.clone(); 21 | output_path.push("image.data"); 22 | fs::write(output_path, data)?; 23 | 24 | // Output the Glulx sources 25 | let glulx_sources = [ 26 | ("files.c", include_str!("../upstream/glulxe/files.c")), 27 | ("float.c", include_str!("../upstream/glulxe/float.c")), 28 | ("funcs.c", include_str!("../upstream/glulxe/funcs.c")), 29 | ("gestalt.c", include_str!("../upstream/glulxe/gestalt.c")), 30 | ("gestalt.h", include_str!("../upstream/glulxe/gestalt.h")), 31 | ("glkop.c", include_str!("../upstream/glulxe/glkop.c")), 32 | ("glulxe.h", include_str!("../upstream/glulxe/glulxe.h")), 33 | ("heap.c", include_str!("../upstream/glulxe/heap.c")), 34 | ("main.c", include_str!("../upstream/glulxe/main.c")), 35 | ("opcodes.h", include_str!("../upstream/glulxe/opcodes.h")), 36 | ("operand.c", include_str!("../upstream/glulxe/operand.c")), 37 | ("osdepend.c", include_str!("../upstream/glulxe/osdepend.c")), 38 | ("profile.c", include_str!("../upstream/glulxe/profile.c")), 39 | ("search.c", include_str!("../upstream/glulxe/search.c")), 40 | ("serial.c", include_str!("../upstream/glulxe/serial.c")), 41 | ("string.c", include_str!("../upstream/glulxe/string.c")), 42 | ("unixstrt.h", include_str!("../upstream/glulxe/unixstrt.h")), 43 | ("vm.c", include_str!("../upstream/glulxe/vm.c")), 44 | ]; 45 | 46 | // Make the output directory if necessary 47 | let mut glulxe_path = self.out_dir.clone(); 48 | glulxe_path.push("glulxe"); 49 | fs::create_dir_all(&glulxe_path)?; 50 | for glulxe_name in &glulx_sources { 51 | let mut output_path = glulxe_path.clone(); 52 | output_path.push(glulxe_name.0); 53 | fs::write(output_path, glulxe_name.1)?; 54 | } 55 | 56 | // Output the template files 57 | let templates = [ 58 | ("CMakeLists.txt", include_str!("templates/CMakeLists.txt")), 59 | ("glulxtoc.h", include_str!("templates/glulxtoc.h")), 60 | ("LICENSE", include_str!("templates/LICENSE")), 61 | ("runtime.c", include_str!("templates/runtime.c")), 62 | ("unixstrt.c", include_str!("templates/unixstrt.c")), 63 | ]; 64 | let replacements = [ 65 | ["GLULXE_FILES", &glulx_sources.iter().map(|(file, _)| *file).collect::>().join(" ")], 66 | ["IMAGE_LENGTH_VALUE", &data.len().to_string()], 67 | ["EXENAME", &self.name], 68 | ]; 69 | 70 | for template_name in &templates { 71 | let mut file = String::from(template_name.1); 72 | for replacement in &replacements { 73 | file = file.replace(replacement[0], replacement[1]); 74 | } 75 | 76 | let mut output_path = self.out_dir.clone(); 77 | output_path.push(template_name.0); 78 | fs::write(output_path, file)?; 79 | } 80 | 81 | let duration = start.elapsed(); 82 | println!("Time outputting files from templates: {:?}", duration); 83 | Ok(()) 84 | } 85 | } -------------------------------------------------------------------------------- /glulxtoc/src/output/functions_common.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Output common functions 4 | ======================= 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use if_decompiler::*; 13 | use glulx::*; 14 | use glulx::opcodes; 15 | 16 | use super::*; 17 | 18 | impl GlulxOutput { 19 | 20 | // Output an instruction body 21 | pub fn output_common_instruction(&self, instruction: &Instruction, args: Vec) -> String { 22 | let opcode = instruction.opcode; 23 | let null = String::from("NULL"); 24 | let op_a = args.get(0).unwrap_or(&null); 25 | let op_b = args.get(1).unwrap_or(&null); 26 | use opcodes::*; 27 | match opcode { 28 | // Following the order of glulxe's exec.c, not strict numerical order 29 | OP_NOP => String::new(), 30 | OP_ADD => args.join(" + "), 31 | OP_SUB => format_safe_stack_pops_expression("{} - {}", &args), 32 | OP_MUL => args.join(" * "), 33 | OP_DIV => runtime("OP_DIV", &args), 34 | OP_MOD => runtime("OP_MOD", &args), 35 | OP_NEG => format!("-((glsi32) {})", op_a), 36 | OP_BITAND => args.join(" & "), 37 | OP_BITOR => args.join(" | "), 38 | OP_BITXOR => args.join(" ^ "), 39 | OP_BITNOT => format!("~{}", op_a), 40 | OP_SHIFTL => runtime("OP_SHIFTL", &args), 41 | OP_USHIFTR => runtime("OP_USHIFTR", &args), 42 | OP_SSHIFTR => runtime("OP_SSHIFTR", &args), 43 | OP_JUMP => String::new(), 44 | OP_JZ => format!("{} == 0", op_a), 45 | OP_JNZ => format!("{} != 0", op_a), 46 | OP_JEQ => format!("{} == {}", op_a, op_b), 47 | OP_JNE => format!("{} != {}", op_a, op_b), 48 | OP_JLT => format_safe_stack_pops_expression("(glsi32) {} < (glsi32) {}", &args), 49 | OP_JGT => format_safe_stack_pops_expression("(glsi32) {} > (glsi32) {}", &args), 50 | OP_JLE => format_safe_stack_pops_expression("(glsi32) {} <= (glsi32) {}", &args), 51 | OP_JGE => format_safe_stack_pops_expression("(glsi32) {} >= (glsi32) {}", &args), 52 | OP_JLTU => format_safe_stack_pops_expression("{} < {}", &args), 53 | OP_JGTU => format_safe_stack_pops_expression("{} > {}", &args), 54 | OP_JLEU => format_safe_stack_pops_expression("{} <= {}", &args), 55 | OP_JGEU => format_safe_stack_pops_expression("{} >= {}", &args), 56 | // OP_CALL 57 | // OP_RETURN 58 | // OP_TAILCALL 59 | // OP_CATCH 60 | // OP_THROW 61 | OP_COPY => op_a.clone(), 62 | // OP_COPYS | OP_COPYB 63 | OP_SEXS => runtime("OP_SEXS", &args), 64 | OP_SEXB => runtime("OP_SEXB", &args), 65 | OP_ALOAD => format_safe_stack_pops_macro("Mem4({} + 4 * (glsi32) {})", &args), 66 | OP_ALOADS => format_safe_stack_pops_macro("Mem2({} + 2 * (glsi32) {})", &args), 67 | OP_ALOADB => format_safe_stack_pops_macro("Mem1({} + (glsi32) {})", &args), 68 | OP_ALOADBIT => runtime("OP_ALOADBIT", &args), 69 | OP_ASTORE => format_safe_stack_pops_expression("store_operand(1, {} + 4 * (glsi32) {}, {})", &args), 70 | OP_ASTORES => format_safe_stack_pops_expression("store_operand_s(1, {} + 2 * (glsi32) {}, {})", &args), 71 | OP_ASTOREB => format_safe_stack_pops_expression("store_operand_b(1, {} + (glsi32) {}, {})", &args), 72 | OP_ASTOREBIT => runtime("OP_ASTOREBIT", &args), 73 | OP_STKCOUNT => String::from("(stackptr - valstackbase) / 4"), 74 | OP_STKPEEK => runtime("OP_STKPEEK", &args), 75 | OP_STKSWAP => runtime("OP_STKSWAP", &args), 76 | OP_STKCOPY => runtime("OP_STKCOPY", &args), 77 | OP_STKROLL => runtime("OP_STKROLL", &args), 78 | // OP_STREAMCHAR ..= OP_STREAMUNICHAR 79 | OP_GESTALT => runtime("do_gestalt", &args), 80 | OP_DEBUGTRAP => format!("fatal_error_i(\"user debugtrap encountered.\", {})", op_a), 81 | OP_JUMPABS => String::new(), 82 | // OP_CALLF ..= OP_CALLFIII 83 | OP_GETMEMSIZE => String::from("endmem"), 84 | OP_SETMEMSIZE => format!("change_memsize({}, 0)", op_a), 85 | OP_GETSTRINGTBL => String::from("stream_get_table()"), 86 | OP_SETSTRINGTBL => runtime("stream_set_table", &args), 87 | // OP_GETIOSYS 88 | OP_SETIOSYS => format!("iosys_mode = {}; stream_set_iosys(iosys_mode, {})", op_a, op_b), 89 | OP_GLK => format!("(temp0 = {}, temp1 = {}, perform_glk(temp0, temp1, pop_arguments(temp1, 0)))", op_a, op_b), 90 | OP_RANDOM => runtime("OP_RANDOM", &args), 91 | OP_SETRANDOM => runtime("glulx_setrandom", &args), 92 | OP_VERIFY => runtime("perform_verify", &args), 93 | // OP_RESTART 94 | OP_PROTECT => runtime("OP_PROTECT", &args), 95 | // OP_SAVE 96 | // OP_RESTORE 97 | // OP_SAVEUNDO 98 | // OP_RESTOREUNDO 99 | // OP_QUIT 100 | OP_LINEARSEARCH => runtime("linear_search", &args), 101 | OP_BINARYSEARCH => runtime("binary_search", &args), 102 | OP_LINKEDSEARCH => runtime("linked_search", &args), 103 | OP_MZERO => runtime("OP_MZERO", &args), 104 | OP_MCOPY => runtime("OP_MCOPY", &args), 105 | OP_MALLOC => runtime("heap_alloc", &args), 106 | OP_MFREE => runtime("heap_free", &args), 107 | OP_ACCELFUNC => runtime("accel_set_func", &args), 108 | OP_ACCELPARAM => runtime("accel_set_param", &args), 109 | OP_NUMTOF => format!("encode_float((gfloat32) ((glsi32) {}))", op_a), 110 | OP_FTONUMZ => runtime("OP_FTONUMZ", &args), 111 | OP_FTONUMN => runtime("OP_FTONUMN", &args), 112 | OP_FADD => format!("encode_float(decode_float({}) + decode_float({}))", op_a, op_b), 113 | OP_FSUB => format!("encode_float(decode_float({}) - decode_float({}))", op_a, op_b), 114 | OP_FMUL => format!("encode_float(decode_float({}) * decode_float({}))", op_a, op_b), 115 | OP_FDIV => format_safe_stack_pops_expression("encode_float(decode_float({}) / decode_float({}))", &args), 116 | // OP_FMOD 117 | OP_FLOOR => runtime_float("floorf", op_a), 118 | OP_CEIL => runtime("OP_CEIL", &args), 119 | OP_SQRT => runtime_float("sqrtf", op_a), 120 | OP_LOG => runtime_float("logf", op_a), 121 | OP_EXP => runtime_float("expf", op_a), 122 | OP_POW => format_safe_stack_pops_expression("encode_float(glulx_powf(decode_float({}), decode_float({})))", &args), 123 | OP_SIN => runtime_float("sinf", op_a), 124 | OP_COS => runtime_float("cosf", op_a), 125 | OP_TAN => runtime_float("tanf", op_a), 126 | OP_ASIN => runtime_float("asinf", op_a), 127 | OP_ACOS => runtime_float("acosf", op_a), 128 | OP_ATAN => runtime_float("atanf", op_a), 129 | OP_ATAN2 => format_safe_stack_pops_expression("encode_float(atan2f(decode_float({}), decode_float({})))", &args), 130 | OP_JISINF => format!("temp0 = {}, temp0 == 0x7F800000 || temp0 == 0xFF800000", op_a), 131 | OP_JISNAN => format!("temp0 = {}, (temp0 & 0x7F800000) == 0x7F800000 && (temp0 & 0x007FFFFF) != 0", op_a), 132 | OP_JFEQ => format_safe_stack_pops_expression("OP_JFEQ({}, {}, {})", &args), 133 | OP_JFNE => format_safe_stack_pops_expression("!OP_JFEQ({}, {}, {})", &args), 134 | OP_JFLT => format_safe_stack_pops_expression("decode_float({}) < decode_float({})", &args), 135 | OP_JFGT => format_safe_stack_pops_expression("decode_float({}) > decode_float({})", &args), 136 | OP_JFLE => format_safe_stack_pops_expression("decode_float({}) <= decode_float({})", &args), 137 | OP_JFGE => format_safe_stack_pops_expression("decode_float({}) >= decode_float({})", &args), 138 | _ => panic!("Unknown opcode {:>3X} at address {}", opcode, instruction.addr), 139 | } 140 | } 141 | } 142 | 143 | fn runtime(name: &str, operands: &Vec) -> String { 144 | let (prelude, new_operands) = safe_stack_pops(operands, false); 145 | if prelude == "" { 146 | return format!("{}({})", name, operands.join(", ")); 147 | } 148 | format!("({}, {}({}))", prelude, name, new_operands.join(", ")) 149 | } 150 | 151 | fn runtime_float(func: &str, operand: &String) -> String { 152 | format!("encode_float({}(decode_float({})))", func, operand) 153 | } -------------------------------------------------------------------------------- /glulxtoc/src/output/functions_unsafe.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Output unsafe functions 4 | ======================= 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use std::fs::File; 13 | use std::io::prelude::*; 14 | use std::io::BufWriter; 15 | use std::time::Instant; 16 | 17 | use if_decompiler::*; 18 | use FunctionSafety::*; 19 | use glulx::*; 20 | use Operand::*; 21 | use glulx::opcodes; 22 | 23 | use super::*; 24 | 25 | impl GlulxOutput { 26 | pub fn output_unsafe_functions(&mut self) -> std::io::Result<()> { 27 | print!("Outputting unsafe functions..."); 28 | io::stdout().flush().unwrap(); 29 | let start = Instant::now(); 30 | 31 | let mut code_file = self.make_file("functions_unsafe.c")?; 32 | 33 | // Output the header 34 | writeln!(code_file, "#include \"glk.h\" 35 | #include \"glulxe.h\" 36 | #include \"glulxtoc.h\" 37 | #include 38 | ")?; 39 | 40 | //let mut warn_about_dynamic_branches = false; 41 | let mut function_chunks = Vec::new(); 42 | 43 | for (chunk_num, chunk) in self.unsafe_functions.chunks(1000).enumerate() { 44 | writeln!(code_file, "static int execute_chunk_{}(void) {{ 45 | glui32 temp0, temp1, temp2, temp3, temp4, temp5; 46 | switch (pc) {{", chunk_num)?; 47 | let (first_func, _warn) = self.output_functions_chunk(&mut code_file, chunk)?; 48 | function_chunks.push(first_func); 49 | /*if warn { 50 | warn_about_dynamic_branches = true; 51 | }*/ 52 | writeln!(code_file, " default: 53 | // Try to recover - if we are jumping into the first address of a safe function we can tailcall it 54 | if (VM_JUMP_CALL(pc)) {{ 55 | break; 56 | }} 57 | fatal_error_i(\"Branched to invalid address:\", pc); 58 | }} 59 | return 0; 60 | }} 61 | ")?; 62 | } 63 | 64 | function_chunks.remove(0); 65 | 66 | writeln!(code_file, "void execute_loop(void) {{ 67 | glui32 temp0, temp1, ret = 0; 68 | while (1) {{ 69 | if (pc == STREAM_HANDLER_FAKE_FUNCTION) {{ 70 | temp0 = PopStack(); 71 | temp1 = PopStack(); 72 | pc = STREAM_HANDLER_RETURN; 73 | switch (temp0) {{ 74 | case STREAM_CHAR: (*stream_char_handler)(temp1 & 0xFF); break; 75 | case STREAM_NUM: stream_num((glsi32) temp1, 0, 0); break; 76 | case STREAM_STRING: stream_string(temp1, 0, 0); break; 77 | case STREAM_UNICHAR: (*stream_unichar_handler)(temp1); break; 78 | }} 79 | }} 80 | else if (pc == STREAM_HANDLER_RETURN) {{ 81 | return; 82 | }}")?; 83 | 84 | for (index, chunk) in function_chunks.iter().enumerate() { 85 | writeln!(code_file, " else if (pc < {}) {{ 86 | ret = execute_chunk_{}(); 87 | }}", chunk, index)?; 88 | } 89 | 90 | writeln!(code_file, " else {{ 91 | ret = execute_chunk_{}(); 92 | }} 93 | if (ret) {{ 94 | return; 95 | }} 96 | }} 97 | }}", function_chunks.len())?; 98 | 99 | // I don't think this warning is needed anymore 100 | /*if warn_about_dynamic_branches { 101 | println!("Warning ❗ This Glulx file features dynamic branches or jumps; please provide an Inform debug file."); 102 | }*/ 103 | 104 | let duration = start.elapsed(); 105 | println!(" completed in {:?}", duration); 106 | Ok(()) 107 | } 108 | 109 | // Output a chunk of functions 110 | fn output_functions_chunk(&self, code_file: &mut BufWriter, functions: &[u32]) -> std::io::Result<(u32, bool)> { 111 | let mut need_to_warn = false; 112 | 113 | // Output the function bodies 114 | for addr in functions { 115 | let function = &self.state.functions[addr]; 116 | 117 | if function.safety == UnsafeDynamicBranches && self.state.debug_function_data.is_none() { 118 | need_to_warn = true; 119 | } 120 | 121 | let name = self.state.debug_function_data.as_ref().map_or(String::new(), |functions| format!(" ({})", functions.get(addr).unwrap().name)); 122 | writeln!(code_file, " // VM Function {}{}", addr, name)?; 123 | 124 | for (label, block) in &function.blocks { 125 | if function.safety != UnsafeDynamicBranches { 126 | writeln!(code_file, " case {}:", label)?; 127 | } 128 | for instruction in &block.code { 129 | let instruction_label = if function.safety == UnsafeDynamicBranches { format!("case {}: ", instruction.addr) } else { String::new() }; 130 | writeln!(code_file, " {}/* {:>3X}/{} */ {};", instruction_label, instruction.opcode, instruction.addr, self.output_instruction_unsafe(&instruction))?; 131 | } 132 | } 133 | } 134 | 135 | Ok((functions[0], need_to_warn)) 136 | } 137 | 138 | // Output an instruction 139 | fn output_instruction_unsafe(&self, instruction: &Instruction) -> String { 140 | let opcode = instruction.opcode; 141 | let operands = self.map_operands_unsafe(instruction); 142 | let null = String::from("NULL"); 143 | let op_a = operands.get(0).unwrap_or(&null); 144 | use opcodes::*; 145 | let body = match opcode { 146 | OP_CALL => self.output_call_unsafe(&operands, instruction), 147 | OP_RETURN => format!("temp0 = {}; leave_function(); if (stackptr == 0) {{return 1;}} pop_callstub(temp0); break", op_a), 148 | OP_TAILCALL => format_safe_stack_pops_statement("VM_TAILCALL_FUNCTION({}, {}); if (stackptr == 0) {{return 1;}} break", &operands), 149 | OP_CATCH => format!("if (OP_CATCH({}, {}, {}, {})) {{return 1;}} break", storer_type(instruction.operands[0]), self.storer_value(instruction.operands[0]), operands[1], instruction.next), 150 | OP_THROW => format!("temp0 = {}; stackptr = {}; pop_callstub(temp0); break", op_a, operands[1]), 151 | OP_COPYS => self.output_copys_unsafe(instruction), 152 | OP_COPYB => self.output_copyb_unsafe(instruction), 153 | OP_STREAMCHAR => format!("if (OP_STREAMX_UNSAFE(STREAM_CHAR, {}, {})) {{break;}}", op_a, instruction.next), 154 | OP_STREAMNUM => format!("if (OP_STREAMX_UNSAFE(STREAM_NUM, {}, {})) {{break;}}", op_a, instruction.next), 155 | OP_STREAMSTR => format!("if (OP_STREAMX_UNSAFE(STREAM_STRING, {}, {})) {{break;}}", op_a, instruction.next), 156 | OP_STREAMUNICHAR => format!("if (OP_STREAMX_UNSAFE(STREAM_UNICHAR, {}, {})) {{break;}}", op_a, instruction.next), 157 | OP_CALLF ..= OP_CALLFIII => self.output_callf_unsafe(instruction, operands), 158 | OP_GETIOSYS => self.output_double_storer_unsafe(instruction, String::from("stream_get_iosys(&temp0, &temp1)")), 159 | OP_RESTART => String::from("vm_restart(); break"), 160 | OP_SAVE => format!("OP_SAVE({}, {}, {}, {})", op_a, instruction.next, storer_type(instruction.storer), self.storer_value(instruction.storer)), 161 | OP_RESTORE => format!("if (OP_RESTORE({}, {}, {})) {{break;}}", op_a, storer_type(instruction.storer), self.storer_value(instruction.storer)), 162 | OP_SAVEUNDO => format!("OP_SAVEUNDO({}, {}, {})", instruction.next, storer_type(instruction.storer), self.storer_value(instruction.storer)), 163 | OP_RESTOREUNDO => format!("if (OP_RESTOREUNDO({}, {})) {{break;}}", storer_type(instruction.storer), self.storer_value(instruction.storer)), 164 | OP_QUIT => String::from("return 1"), 165 | OP_FMOD => self.output_double_storer_unsafe(instruction, format_safe_stack_pops_expression("OP_FMOD({}, {}, &temp0, &temp1)", &operands)), 166 | _ => self.output_storer_unsafe(instruction.storer, self.output_common_instruction(instruction, operands)), 167 | }; 168 | self.output_branch_unsafe(instruction, body) 169 | } 170 | 171 | // Map operands into strings 172 | fn map_operands_unsafe(&self, instruction: &Instruction) -> Vec { 173 | instruction.operands.iter().map(|&operand| self.output_operand_unsafe(operand)).collect() 174 | } 175 | 176 | fn output_operand_unsafe(&self, operand: Operand) -> String { 177 | match operand { 178 | Constant(val) => val.to_string(), 179 | Memory(addr) => format!("Mem4({})", addr), 180 | Stack => String::from("PopStack()"), 181 | Local(addr) => format!("ReadLocal({})", addr), 182 | RAM(addr) => format!("Mem4({})", addr + self.ramstart), 183 | } 184 | } 185 | 186 | fn output_storer_unsafe(&self, storer: Operand, inner: String) -> String { 187 | match storer { 188 | Constant(_) => inner, // Must still output the inner code in case there are side-effects 189 | Memory(addr) => format!("store_operand(1, {}, {})", addr, inner), 190 | Stack => format!("PushStack({})", inner), 191 | Local(addr) => format!("store_operand(2, {}, {})", addr, inner), 192 | RAM(addr) => format!("store_operand(1, {}, {})", addr + self.ramstart, inner), 193 | } 194 | } 195 | 196 | fn output_double_storer_unsafe(&self, instruction: &Instruction, inner: String) -> String { 197 | let store = |storer: Operand, i: u32| { 198 | match storer { 199 | Constant(_) => String::from("NULL"), 200 | Memory(addr) => format!("store_operand(1, {}, temp{})", addr, i), 201 | Stack => format!("PushStack(temp{})", i), 202 | Local(addr) => format!("store_operand(2, {}, temp{})", addr, i), 203 | RAM(addr) => format!("store_operand(1, {}, temp{})", addr + self.ramstart, i), 204 | } 205 | }; 206 | format!("{}; {}; {}", inner, store(instruction.storer, 0), store(instruction.storer2, 1)) 207 | } 208 | 209 | fn output_branch_unsafe(&self, instruction: &Instruction, condition: String) -> String { 210 | use opcodes::*; 211 | match instruction.branch { 212 | None => condition, 213 | Some(target) => match instruction.opcode { 214 | OP_CATCH => condition, 215 | OP_JUMP => format!("{}; break", self.output_branch_action_unsafe(instruction, target)), 216 | OP_JUMPABS => format!("pc = {}; break", self.output_operand_unsafe(*instruction.operands.last().unwrap())), 217 | _ => format!("if ({}) {{{}; break;}}", condition, self.output_branch_action_unsafe(instruction, target)), 218 | }, 219 | } 220 | } 221 | 222 | fn output_branch_action_unsafe(&self, instruction: &Instruction, branch: BranchTarget) -> String { 223 | use BranchTarget::*; 224 | match branch { 225 | Dynamic => format!("if (VM_BRANCH({}, {})) {{return 1;}}", self.output_operand_unsafe(*instruction.operands.last().unwrap()), instruction.next), 226 | Absolute(addr) => format!("pc = {}", addr), 227 | Return(val) => format!("temp0 = {}; leave_function(); if (stackptr == 0) {{return 1;}} pop_callstub(temp0)", val), 228 | } 229 | } 230 | 231 | fn output_call_unsafe(&self, operands: &Vec, instruction: &Instruction) -> String { 232 | let (prelude, new_operands) = safe_stack_pops(operands, false); 233 | let prelude_out = if prelude == "" { String::new() } else { format!("{}; ", prelude) }; 234 | format!("{}if (VM_CALL_FUNCTION({}, {}, {}, {}, {})) {{break;}}", prelude_out, new_operands[0], new_operands[1], storer_type(instruction.storer), self.storer_value(instruction.storer), instruction.next) 235 | } 236 | 237 | fn output_callf_unsafe(&self, instruction: &Instruction, operands: Vec) -> String { 238 | let (prelude, new_operands) = safe_stack_pops(&operands, false); 239 | let inner = match operands.len() { 240 | 1 => format!("VM_CALL_FUNCTION({}, 0", operands[0]), 241 | 2 => format!("OP_CALLFI({}", new_operands.join(", ")), 242 | 3 => format!("OP_CALLFII({}", new_operands.join(", ")), 243 | 4 => format!("OP_CALLFIII({}", new_operands.join(", ")), 244 | _ => unreachable!(), 245 | }; 246 | let prelude_out = if prelude == "" { String::new() } else { format!("{}; ", prelude) }; 247 | format!("{}if ({}, {}, {}, {})) {{break;}}", prelude_out, inner, storer_type(instruction.storer), self.storer_value(instruction.storer), instruction.next) 248 | } 249 | 250 | fn output_copys_unsafe(&self, instruction: &Instruction) -> String { 251 | let inner = match instruction.operands[0] { 252 | Constant(val) => format!("{} & 0xFFFF", val), 253 | Memory(addr) => format!("Mem2({})", addr), 254 | Stack => String::from("PopStack() & 0xFFFF"), 255 | Local(addr) => format!("Stk2({} + localsbase)", addr), 256 | RAM(addr) => format!("Mem2({})", addr + self.ramstart), 257 | }; 258 | match instruction.operands[1] { 259 | Constant(_) => inner, 260 | Memory(addr) => format!("store_operand_s(1, {}, {})", addr, inner), 261 | Stack => format!("PushStack({})", inner), 262 | Local(addr) => format!("store_operand_s(2, {}, {})", addr, inner), 263 | RAM(addr) => format!("store_operand_s(1, {}, {})", addr + self.ramstart, inner), 264 | } 265 | } 266 | 267 | fn output_copyb_unsafe(&self, instruction: &Instruction) -> String { 268 | let inner = match instruction.operands[0] { 269 | Constant(val) => format!("{} & 0xFF", val), 270 | Memory(addr) => format!("Mem1({})", addr), 271 | Stack => String::from("PopStack() & 0xFF"), 272 | Local(addr) => format!("Stk1({} + localsbase)", addr), 273 | RAM(addr) => format!("Mem1({})", addr + self.ramstart), 274 | }; 275 | match instruction.operands[1] { 276 | Constant(_) => inner, 277 | Memory(addr) => format!("store_operand_b(1, {}, {})", addr, inner), 278 | Stack => format!("PushStack({})", inner), 279 | Local(addr) => format!("store_operand_b(2, {}, {})", addr, inner), 280 | RAM(addr) => format!("store_operand_b(1, {}, {})", addr + self.ramstart, inner), 281 | } 282 | } 283 | 284 | fn storer_value(&self, storer: Operand) -> u32 { 285 | match storer { 286 | Constant(_) | Stack => 0, 287 | Memory(val) | Local(val) => val, 288 | RAM(val) => val + self.ramstart, 289 | } 290 | } 291 | } 292 | 293 | fn storer_type(storer: Operand) -> u32 { 294 | match storer { 295 | Constant(_) => 0, 296 | Memory(_) | RAM(_) => 1, 297 | Local(_) => 2, 298 | Stack => 3, 299 | } 300 | } -------------------------------------------------------------------------------- /glulxtoc/src/output/image.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Output the storyfile as a C array 4 | ================================= 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use std::io::prelude::*; 13 | use std::time::Instant; 14 | 15 | use super::*; 16 | 17 | impl GlulxOutput { 18 | pub fn output_image(&self, data: &[u8]) -> std::io::Result<()> { 19 | print!("Outputting image.c..."); 20 | io::stdout().flush().unwrap(); 21 | let start = Instant::now(); 22 | 23 | let mut file = self.make_file("image.c")?; 24 | 25 | writeln!(file, "char GLULX_IMAGE[] = {{")?; 26 | 27 | let image_iter = data.chunks(16); 28 | for row in image_iter { 29 | let row_text = format!("{:?}", row); 30 | writeln!(file, " {},", &row_text[1..(row_text.len()-1)])?; 31 | } 32 | 33 | write!(file, "}};")?; 34 | 35 | let duration = start.elapsed(); 36 | println!(" completed in {:?}", duration); 37 | Ok(()) 38 | } 39 | } -------------------------------------------------------------------------------- /glulxtoc/src/output/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Output C files 4 | ============== 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use std::fs; 13 | use std::io; 14 | use std::path::PathBuf; 15 | 16 | use dyn_fmt::AsStrFormatExt; 17 | 18 | use if_decompiler::*; 19 | use glulx::GlulxState; 20 | 21 | mod files; 22 | mod functions_common; 23 | mod functions_safe; 24 | mod functions_unsafe; 25 | //mod image; 26 | 27 | pub struct GlulxOutput { 28 | pub disassemble_mode: bool, 29 | pub file_length: u32, 30 | pub name: String, 31 | pub out_dir: PathBuf, 32 | pub ramstart: u32, 33 | pub safe_functions: Vec, 34 | pub state: GlulxState, 35 | pub unsafe_functions: Vec, 36 | } 37 | 38 | impl GlulxOutput { 39 | pub fn new(disassemble_mode: bool, file_length: u32, name: String, out_dir: PathBuf, state: GlulxState) -> GlulxOutput { 40 | let mut safe_functions = Vec::new(); 41 | let mut unsafe_functions = Vec::new(); 42 | for (&addr, function) in &state.functions { 43 | if !disassemble_mode && function.safety == FunctionSafety::SafetyTBD { 44 | safe_functions.push(addr); 45 | } 46 | else { 47 | unsafe_functions.push(addr); 48 | } 49 | } 50 | GlulxOutput { 51 | disassemble_mode, 52 | file_length, 53 | name, 54 | out_dir, 55 | ramstart: state.ramstart, 56 | safe_functions, 57 | state, 58 | unsafe_functions, 59 | } 60 | } 61 | 62 | pub fn output(&mut self, file: &[u8]) -> io::Result<()> { 63 | // Make the output directory if necessary 64 | fs::create_dir_all(&self.out_dir)?; 65 | 66 | self.output_from_templates(file)?; 67 | self.output_safe_functions()?; 68 | self.output_unsafe_functions()?; 69 | Ok(()) 70 | } 71 | 72 | // A little helper function for making files in the output dir 73 | fn make_file(&self, name: &str) -> io::Result> { 74 | let mut path = self.out_dir.clone(); 75 | path.push(name); 76 | let file = fs::File::create(path)?; 77 | Ok(io::BufWriter::new(file)) 78 | } 79 | } 80 | 81 | // C says that the order function arguments are evaluated is undefined, which breaks stack pops 82 | // This function takes a Vec of operand strings, and fixes them to ensure the order is right 83 | fn safe_stack_pops(operands: &Vec, in_macro: bool) -> (String, Vec) { 84 | let safe_pops = if in_macro { 0 } else { 1 }; 85 | let mut stack_operands = 0; 86 | for operand in operands { 87 | if operand == "PopStack()" { 88 | stack_operands += 1; 89 | } 90 | } 91 | if stack_operands <= safe_pops { 92 | let mut new_operands = Vec::default(); 93 | for operand in operands { 94 | new_operands.push(operand.clone()); 95 | } 96 | return (String::new(), new_operands); 97 | } 98 | 99 | // Build the new operands 100 | let mut prelude = Vec::default(); 101 | let mut new_operands = Vec::default(); 102 | let mut op = 0; 103 | for operand in operands { 104 | if operand == "PopStack()" && op + safe_pops < stack_operands { 105 | prelude.push(format!("temp{} = PopStack()", op)); 106 | new_operands.push(format!("temp{}", op)); 107 | op += 1; 108 | } 109 | else { 110 | new_operands.push(operand.clone()); 111 | } 112 | } 113 | (prelude.join(", "), new_operands) 114 | } 115 | 116 | // And then a function to use the above with a format string for an expression 117 | fn format_safe_stack_pops_expression(format: &str, operands: &Vec) -> String { 118 | let (prelude, new_operands) = safe_stack_pops(operands, false); 119 | if prelude == "" { 120 | return format.format(operands); 121 | } 122 | format!("({}, {})", prelude, format.format(&new_operands)) 123 | } 124 | 125 | // Now an expression that uses a macro (such as Mem4) 126 | fn format_safe_stack_pops_macro(format: &str, operands: &Vec) -> String { 127 | let (prelude, new_operands) = safe_stack_pops(operands, true); 128 | if prelude == "" { 129 | return format.format(operands); 130 | } 131 | format!("({}, {})", prelude, format.format(&new_operands)) 132 | } 133 | 134 | // And the same but for a statement 135 | fn format_safe_stack_pops_statement(format: &str, operands: &Vec) -> String { 136 | let (prelude, new_operands) = safe_stack_pops(operands, false); 137 | if prelude == "" { 138 | return format.format(operands); 139 | } 140 | format!("{}; {}", prelude, format.format(&new_operands)) 141 | } -------------------------------------------------------------------------------- /glulxtoc/src/output/templates/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | project(NAME) 4 | 5 | # Add sources to a target from a specified directory 6 | function(add_sources target dir) 7 | cmake_parse_arguments(PARSE_ARGV 2 ADD "" "" "SRCS") 8 | list(TRANSFORM ADD_SRCS PREPEND ${dir}) 9 | target_sources(${target} PRIVATE ${ADD_SRCS}) 10 | target_include_directories(${target} PRIVATE ${dir}) 11 | endfunction() 12 | 13 | # Find the requested library 14 | set(GlkLibPath "glk" CACHE PATH "Glk Library Path") 15 | set(GlkLibName "" CACHE STRING "Glk Library Name (without lib- or -.a)") 16 | if (GlkLibName STREQUAL "") 17 | get_filename_component(GlkLibNameReal ${GlkLibPath} NAME) 18 | else () 19 | set(GlkLibNameReal ${GlkLibName} STRING) 20 | endif() 21 | add_library(glk STATIC IMPORTED) 22 | set_target_properties(glk PROPERTIES IMPORTED_LOCATION "${GlkLibPath}/lib${GlkLibNameReal}.a") 23 | target_include_directories(glk INTERFACE ${GlkLibPath}) 24 | 25 | # Prepare the image data as a library 26 | add_library(image STATIC image.o) 27 | set_target_properties(image PROPERTIES LINKER_LANGUAGE C) 28 | add_custom_command(OUTPUT image.o 29 | COMMAND cd ${CMAKE_CURRENT_SOURCE_DIR} && ld -r -b binary -o ${CMAKE_CURRENT_BINARY_DIR}/image.o image.data 30 | COMMAND objcopy --rename-section .data=.rodata,alloc,load,readonly,data,contents ${CMAKE_CURRENT_BINARY_DIR}/image.o ${CMAKE_CURRENT_BINARY_DIR}/image.o) 31 | set_source_files_properties(image.o PROPERTIES EXTERNAL_OBJECT true GENERATED true) 32 | 33 | # And now our project 34 | add_executable(EXENAME) 35 | target_link_libraries(EXENAME glk image m) 36 | add_sources(EXENAME "glulxe/" SRCS GLULXE_FILES) 37 | target_include_directories(EXENAME PRIVATE glulxe/) 38 | add_sources(EXENAME "./" 39 | SRCS functions_safe.c functions_unsafe.c runtime.c unixstrt.c) 40 | target_compile_definitions(EXENAME PRIVATE OS_UNIX FLOAT_COMPILE_SAFER_POWF) 41 | target_compile_options(EXENAME PRIVATE -Wall -Wmissing-prototypes 42 | -Wstrict-prototypes -Wno-overflow -Wno-unused) 43 | if (CMAKE_C_COMPILER_ID MATCHES "Clang") 44 | target_compile_options(EXENAME PRIVATE 45 | -fbracket-depth=5000 46 | -Wno-constant-conversion -Wno-integer-overflow) 47 | set_source_files_properties(functions_safe.c functions_unsafe.c 48 | PROPERTIES COMPILE_OPTIONS "-Wconditional-uninitialized") 49 | endif() -------------------------------------------------------------------------------- /glulxtoc/src/output/templates/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 1999-2016, Andrew Plotkin 4 | Copyright (c) 2021, Dannii Willis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /glulxtoc/src/output/templates/glulxtoc.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | C output files from glulxtoc 4 | ============================ 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | #include "glk.h" 13 | 14 | #define ReadLocal(addr) (Stk4(addr + localsbase)) 15 | 16 | #define CALL_FUNC(code, pre_pushed_args) (oldsp = stackptr, oldvsb = valstackbase, valstackbase = stackptr - pre_pushed_args * 4, res = code, stackptr = oldsp - pre_pushed_args * 4, valstackbase = oldvsb, res) 17 | 18 | // Fake addresses for streaming 19 | #define STREAM_HANDLER_FAKE_FUNCTION (0xffffffff) 20 | #define STREAM_HANDLER_RETURN (0xfffffffe) 21 | #define STREAM_CHAR 0 22 | #define STREAM_NUM 1 23 | #define STREAM_STRING 2 24 | #define STREAM_UNICHAR 3 25 | 26 | // functions_safe.c 27 | extern int VM_FUNC_IS_SAFE(glui32 addr); 28 | extern int VM_FUNC_IS_SAFE_VARARGS(glui32 addr); 29 | extern glui32 VM_FUNC_SUBTRACT_HEADER(glui32 pc); 30 | extern glui32 VM_CALL_SAFE_FUNCTION_WITH_STACK_ARGS(glui32 addr, glui32 count); 31 | 32 | // image.data 33 | #define GLULX_IMAGE_LENGTH IMAGE_LENGTH_VALUE 34 | extern char _binary_image_data_start[]; 35 | #define GLULX_IMAGE _binary_image_data_start 36 | 37 | // runtime.c 38 | extern int iosys_mode; 39 | extern glui32 OP_DIV(glui32 arg1, glui32 arg2); 40 | extern glui32 OP_MOD(glui32 arg1, glui32 arg2); 41 | extern glui32 OP_SHIFTL(glui32 arg1, glui32 arg2); 42 | extern glui32 OP_USHIFTR(glui32 arg1, glui32 arg2); 43 | extern glui32 OP_SSHIFTR(glui32 arg0, glui32 arg1); 44 | extern int OP_CATCH(glui32 storetype, glui32 storeval, glui32 offset, glui32 next); 45 | extern glui32 OP_SEXS(glui32 arg0); 46 | extern glui32 OP_SEXB(glui32 arg0); 47 | extern glui32 OP_ALOADBIT(glui32 arg0, glui32 arg1); 48 | extern void OP_ASTOREBIT(glui32 arg0, glui32 arg1, glui32 arg2); 49 | extern glui32 OP_STKPEEK(glui32 arg0); 50 | extern void OP_STKSWAP(void); 51 | extern void OP_STKCOPY(glui32 arg0); 52 | extern void OP_STKROLL(glui32 arg0, glui32 arg1); 53 | extern void OP_STREAMX_SAFE(int mode, glui32 val); 54 | extern int OP_STREAMX_UNSAFE(int mode, glui32 val, glui32 next); 55 | extern glui32 OP_RANDOM(glui32 arg0); 56 | extern void OP_PROTECT(glui32 arg0, glui32 arg1); 57 | extern void OP_MZERO(glui32 arg0, glui32 arg1); 58 | extern void OP_SAVE(glui32 arg0, glui32 next, glui32 storetype, glui32 storeval); 59 | extern int OP_RESTORE(glui32 arg0, glui32 storetype, glui32 storeval); 60 | extern void OP_SAVEUNDO(glui32 next, glui32 storetype, glui32 storeval); 61 | extern int OP_RESTOREUNDO(glui32 storetype, glui32 storeval); 62 | extern int OP_CALLFI(glui32 addr, glui32 arg0, glui32 storetype, glui32 storeval, glui32 next); 63 | extern int OP_CALLFII(glui32 addr, glui32 arg0, glui32 arg1, glui32 storetype, glui32 storeval, glui32 next); 64 | extern int OP_CALLFIII(glui32 addr, glui32 arg0, glui32 arg1, glui32 arg2, glui32 storetype, glui32 storeval, glui32 next); 65 | extern void OP_MCOPY(glui32 arg0, glui32 arg1, glui32 arg2); 66 | extern glsi32 OP_FTONUMZ(glui32 arg0); 67 | extern glsi32 OP_FTONUMN(glui32 arg0); 68 | extern void OP_FMOD(glui32 arg0, glui32 arg1, glui32 *dest0, glui32 *dest1); 69 | extern glui32 OP_CEIL(glui32 arg0); 70 | extern glui32 OP_JFEQ(glui32 arg0, glui32 arg1, glui32 arg2); 71 | extern glui32 PopStack(void); 72 | extern void PushStack(glui32 storeval); 73 | extern int VM_BRANCH(glui32 offset, glui32 next); 74 | extern int VM_CALL_FUNCTION(glui32 addr, glui32 count, glui32 storetype, glui32 storeval, glui32 next); 75 | extern int VM_JUMP_CALL(glui32 pc); 76 | extern void VM_TAILCALL_FUNCTION(glui32 addr, glui32 count); -------------------------------------------------------------------------------- /glulxtoc/src/output/templates/runtime.c: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Runtime functions - mostly things that used to be in exec.c 4 | =========================================================== 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | Copyright (c) 1999-2016, Andrew Plotkin 8 | MIT licenced 9 | https://github.com/curiousdannii/if-decompiler 10 | 11 | */ 12 | 13 | #include "glk.h" 14 | #include "glulxe.h" 15 | #include "glulxtoc.h" 16 | #include 17 | 18 | int iosys_mode; 19 | 20 | // From accel.c 21 | void init_accel(void) {} 22 | acceleration_func accel_find_func(glui32 index) { return NULL; } 23 | acceleration_func accel_get_func(glui32 addr) { return NULL; } 24 | void accel_set_func(glui32 index, glui32 addr) {} 25 | void accel_set_param(glui32 index, glui32 val) {} 26 | glui32 accel_get_param_count(void) { return 0; } 27 | glui32 accel_get_param(glui32 index) { return 0; } 28 | void accel_iterate_funcs(void (*func)(glui32 index, glui32 addr)) {} 29 | 30 | glui32 OP_DIV(glui32 arg0, glui32 arg1) { 31 | glsi32 dividend = (glsi32) arg0; 32 | glsi32 divisor = (glsi32) arg1; 33 | if (divisor == 0) { 34 | fatal_error("Division by zero."); 35 | } 36 | /* Since C doesn't guarantee the results of division of negative 37 | numbers, we carefully convert everything to positive values 38 | first. They have to be unsigned values, too, otherwise the 39 | 0x80000000 case goes wonky. */ 40 | glui32 value, val0, val1; 41 | if (dividend < 0) { 42 | val0 = (-dividend); 43 | if (divisor < 0) { 44 | val1 = (-divisor); 45 | value = val0 / val1; 46 | } 47 | else { 48 | val1 = divisor; 49 | value = -(val0 / val1); 50 | } 51 | } else { 52 | val0 = dividend; 53 | if (divisor < 0) { 54 | val1 = (-divisor); 55 | value = -(val0 / val1); 56 | } 57 | else { 58 | val1 = divisor; 59 | value = val0 / val1; 60 | } 61 | } 62 | return value; 63 | } 64 | 65 | glui32 OP_MOD(glui32 arg0, glui32 arg1) { 66 | glsi32 dividend = (glsi32) arg0; 67 | glsi32 divisor = (glsi32) arg1; 68 | glui32 value, val0, val1; 69 | if (divisor == 0) { 70 | fatal_error("Division by zero doing remainder."); 71 | } 72 | if (divisor < 0) { 73 | val1 = -divisor; 74 | } 75 | else { 76 | val1 = divisor; 77 | } 78 | if (dividend < 0) { 79 | val0 = (-dividend); 80 | value = -(val0 % val1); 81 | } 82 | else { 83 | val0 = dividend; 84 | value = val0 % val1; 85 | } 86 | return value; 87 | } 88 | 89 | glui32 OP_SHIFTL(glui32 arg0, glui32 arg1) { 90 | glsi32 vals0 = (glsi32) arg1; 91 | if (vals0 < 0 || vals0 >= 32) { 92 | return 0; 93 | } 94 | return (glui32) arg0 << (glui32) vals0; 95 | } 96 | 97 | glui32 OP_USHIFTR(glui32 arg0, glui32 arg1) { 98 | glsi32 vals0 = (glsi32) arg1; 99 | if (vals0 < 0 || vals0 >= 32) { 100 | return 0; 101 | } 102 | return (glui32) arg0 >> (glui32) vals0; 103 | } 104 | 105 | glui32 OP_SSHIFTR(glui32 arg0, glui32 arg1) { 106 | glsi32 vals0 = (glsi32) arg1; 107 | if (vals0 < 0 || vals0 >= 32) { 108 | if (arg0 & 0x80000000) 109 | { 110 | return 0xFFFFFFFF; 111 | } else { 112 | return 0; 113 | } 114 | } 115 | /* This is somewhat foolhardy -- C doesn't guarantee that 116 | right-shifting a signed value replicates the sign bit. 117 | We'll assume it for now. */ 118 | return (glsi32) arg0 >> (glsi32) vals0; 119 | } 120 | 121 | int OP_CATCH(glui32 storetype, glui32 storeval, glui32 offset, glui32 next) { 122 | pc = next; 123 | push_callstub(storetype, storeval); 124 | store_operand(storetype, storeval, stackptr); 125 | return VM_BRANCH(offset, next); 126 | } 127 | 128 | glui32 OP_SEXS(glui32 arg0) { 129 | if (arg0 & 0x8000) 130 | { 131 | return arg0 |= 0xFFFF0000; 132 | } 133 | return arg0 &= 0x0000FFFF; 134 | } 135 | 136 | glui32 OP_SEXB(glui32 arg0) { 137 | if (arg0 & 0x80) 138 | { 139 | return arg0 |= 0xFFFFFF00; 140 | } 141 | return arg0 &= 0x000000FF; 142 | } 143 | 144 | glui32 OP_ALOADBIT(glui32 arg0, glui32 arg1) { 145 | glsi32 vals0 = (glsi32) arg1; 146 | glui32 val1 = (vals0 & 7); 147 | if (vals0 >= 0) { 148 | arg0 += (vals0 >> 3); 149 | } else { 150 | arg0 -= (1 + ((-1 - vals0) >> 3)); 151 | } 152 | if (Mem1(arg0) & (1 << val1)) 153 | { 154 | return 1; 155 | } else { 156 | return 0; 157 | } 158 | } 159 | 160 | void OP_ASTOREBIT(glui32 arg0, glui32 arg1, glui32 arg2) { 161 | glsi32 vals0 = (glsi32) arg1; 162 | glui32 val1 = (vals0 & 7); 163 | if (vals0 >= 0) { 164 | arg0 += (vals0 >> 3); 165 | } else { 166 | arg0 -= (1 + ((-1 - vals0) >> 3)); 167 | } 168 | glui32 val0 = Mem1(arg0); 169 | if (arg2) { 170 | val0 |= (1 << val1); 171 | } else { 172 | val0 &= ~((glui32)(1 << val1)); 173 | } 174 | MemW1(arg0, val0); 175 | } 176 | 177 | glui32 OP_STKPEEK(glui32 arg0) { 178 | glsi32 vals0 = arg0 * 4; 179 | if (vals0 < 0 || vals0 >= (stackptr - valstackbase)) 180 | { 181 | fatal_error("Stkpeek outside current stack range."); 182 | } 183 | return Stk4(stackptr - (vals0 + 4)); 184 | } 185 | 186 | void OP_STKSWAP(void) { 187 | if (stackptr < valstackbase + 8) { 188 | fatal_error("Stack underflow in stkswap."); 189 | } 190 | glui32 val0 = Stk4(stackptr - 4); 191 | glui32 val1 = Stk4(stackptr - 8); 192 | StkW4(stackptr - 4, val1); 193 | StkW4(stackptr - 8, val0); 194 | } 195 | 196 | void OP_STKCOPY(glui32 arg0) { 197 | glsi32 vals0 = (glsi32) arg0; 198 | if (vals0 < 0) { 199 | fatal_error("Negative operand in stkcopy."); 200 | } 201 | if (vals0 == 0) { 202 | return; 203 | } 204 | if (stackptr < valstackbase + vals0 * 4) { 205 | fatal_error("Stack underflow in stkcopy."); 206 | } 207 | if (stackptr + vals0 * 4 > stacksize) { 208 | fatal_error("Stack overflow in stkcopy."); 209 | } 210 | glui32 addr = stackptr - vals0 * 4; 211 | for (glui32 ix = 0; ix < vals0; ix++) { 212 | glui32 value = Stk4(addr + ix * 4); 213 | StkW4(stackptr + ix * 4, value); 214 | } 215 | stackptr += vals0 * 4; 216 | } 217 | 218 | void OP_STKROLL(glui32 arg0, glui32 arg1) { 219 | glsi32 vals0 = (glsi32) arg0; 220 | glsi32 vals1 = (glsi32) arg1; 221 | if (vals0 < 0) { 222 | fatal_error("Negative operand in stkroll."); 223 | } 224 | if (stackptr < valstackbase + vals0 * 4) { 225 | fatal_error("Stack underflow in stkroll."); 226 | } 227 | if (vals0 == 0) { 228 | return; 229 | } 230 | /* The following is a bit ugly. We want to do vals1 = vals0-vals1, 231 | because rolling down is sort of easier than rolling up. But 232 | we also want to take the result mod vals0. The % operator is 233 | annoying for negative numbers, so we need to do this in two 234 | cases. */ 235 | if (vals1 > 0) { 236 | vals1 = vals1 % vals0; 237 | vals1 = (vals0) - vals1; 238 | } 239 | else { 240 | vals1 = (-vals1) % vals0; 241 | } 242 | if (vals1 == 0) 243 | { 244 | return; 245 | } 246 | glui32 addr = stackptr - vals0 * 4; 247 | for (glui32 ix = 0; ix < vals1; ix++) { 248 | glui32 value = Stk4(addr + ix * 4); 249 | StkW4(stackptr + ix * 4, value); 250 | } 251 | for (glui32 ix=0; ix < vals0; ix++) { 252 | glui32 value = Stk4(addr + (vals1 + ix) * 4); 253 | StkW4(addr + ix * 4, value); 254 | } 255 | } 256 | 257 | void OP_STREAMX_SAFE(int mode, glui32 val) { 258 | // Shortcut for safe streaming 259 | if (iosys_mode != 1 /* iosys_Filter */) { 260 | switch (mode) { 261 | case STREAM_CHAR: (*stream_char_handler)(val & 0xFF); return; 262 | case STREAM_NUM: stream_num((glsi32) val, 0, 0); return; 263 | case STREAM_UNICHAR: (*stream_unichar_handler)(val); return; 264 | } 265 | } 266 | 267 | // Save the current stack 268 | glui32 oldframeptr = frameptr; 269 | glui32 oldlocalsbase = localsbase; 270 | unsigned char *oldstack = stack; 271 | glui32 oldstackptr = stackptr; 272 | glui32 oldstacksize = stacksize; 273 | glui32 oldvalstackbase = valstackbase; 274 | 275 | // Pretend we're calling execute_loop for the very first time 276 | stack += stackptr; 277 | stacksize -= stackptr; 278 | frameptr = 0; 279 | localsbase = 0; 280 | stackptr = 0; 281 | valstackbase = 0; 282 | 283 | // Fake call the printing handler 284 | pc = STREAM_HANDLER_FAKE_FUNCTION; 285 | PushStack(val); 286 | PushStack(mode); 287 | execute_loop(); 288 | 289 | // And restore the original stack 290 | frameptr = oldframeptr; 291 | localsbase = oldlocalsbase; 292 | stack = oldstack; 293 | stackptr = oldstackptr; 294 | stacksize = oldstacksize; 295 | valstackbase = oldvalstackbase; 296 | } 297 | 298 | int OP_STREAMX_UNSAFE(int mode, glui32 val, glui32 next) { 299 | pc = next; 300 | switch (mode) { 301 | case STREAM_CHAR: (*stream_char_handler)(val & 0xFF); break; 302 | case STREAM_NUM: stream_num((glsi32) val, 0, 0); break; 303 | case STREAM_STRING: stream_string(val, 0, 0); break; 304 | case STREAM_UNICHAR: (*stream_unichar_handler)(val); break; 305 | } 306 | return pc != next; 307 | } 308 | 309 | glui32 OP_RANDOM(glui32 arg0) { 310 | glsi32 vals0 = (glsi32) arg0; 311 | if (vals0 == 0) { 312 | return glulx_random(); 313 | } else if (vals0 >= 1) { 314 | return glulx_random() % (glui32) (vals0); 315 | } else { 316 | return -(glulx_random() % (glui32) (-vals0)); 317 | } 318 | } 319 | 320 | void OP_PROTECT(glui32 arg0, glui32 arg1) { 321 | glui32 val1 = arg0 + arg1; 322 | if (arg0 == val1) { 323 | arg0 = 0; 324 | val1 = 0; 325 | } 326 | protectstart = arg0; 327 | protectend = val1; 328 | } 329 | 330 | void OP_SAVE(glui32 arg0, glui32 next, glui32 storetype, glui32 storeval) { 331 | pc = next; 332 | push_callstub(storetype, storeval); 333 | pop_callstub(perform_save(find_stream_by_id(arg0))); 334 | } 335 | 336 | int OP_RESTORE(glui32 arg0, glui32 storetype, glui32 storeval) { 337 | glui32 value = perform_restore(find_stream_by_id(arg0), FALSE); 338 | if (value == 0) { 339 | /* We've succeeded, and the stack now contains the callstub 340 | saved during saveundo. Ignore this opcode's operand. */ 341 | value = -1; 342 | pop_callstub(value); 343 | return 1; 344 | } 345 | else { 346 | /* We've failed, so we must store the failure in this opcode's 347 | operand. */ 348 | store_operand(storetype, storeval, value); 349 | return 0; 350 | } 351 | } 352 | 353 | void OP_SAVEUNDO(glui32 next, glui32 storetype, glui32 storeval) { 354 | pc = next; 355 | push_callstub(storetype, storeval); 356 | pop_callstub(perform_saveundo()); 357 | } 358 | 359 | int OP_RESTOREUNDO(glui32 storetype, glui32 storeval) { 360 | glui32 value = perform_restoreundo(); 361 | if (value == 0) { 362 | /* We've succeeded, and the stack now contains the callstub 363 | saved during saveundo. Ignore this opcode's operand. */ 364 | value = -1; 365 | pop_callstub(value); 366 | return 1; 367 | } 368 | else { 369 | /* We've failed, so we must store the failure in this opcode's 370 | operand. */ 371 | store_operand(storetype, storeval, value); 372 | return 0; 373 | } 374 | } 375 | 376 | int OP_CALLFI(glui32 addr, glui32 arg0, glui32 storetype, glui32 storeval, glui32 next) { 377 | PushStack(arg0); 378 | return VM_CALL_FUNCTION(addr, 1, storetype, storeval, next); 379 | } 380 | 381 | int OP_CALLFII(glui32 addr, glui32 arg0, glui32 arg1, glui32 storetype, glui32 storeval, glui32 next) { 382 | PushStack(arg1); 383 | PushStack(arg0); 384 | return VM_CALL_FUNCTION(addr, 2, storetype, storeval, next); 385 | } 386 | 387 | int OP_CALLFIII(glui32 addr, glui32 arg0, glui32 arg1, glui32 arg2, glui32 storetype, glui32 storeval, glui32 next) { 388 | PushStack(arg2); 389 | PushStack(arg1); 390 | PushStack(arg0); 391 | return VM_CALL_FUNCTION(addr, 3, storetype, storeval, next); 392 | } 393 | 394 | void OP_MZERO(glui32 arg0, glui32 arg1) { 395 | glui32 lx; 396 | for (lx=0; lx < arg0; lx++, arg1++) { 397 | MemW1(arg1, 0); 398 | } 399 | } 400 | 401 | void OP_MCOPY(glui32 arg0, glui32 arg1, glui32 arg2) { 402 | glui32 lx; 403 | if (arg2 < arg1) { 404 | for (lx = 0; lx < arg0; lx++, arg1++, arg2++) { 405 | MemW1(arg2, Mem1(arg1)); 406 | } 407 | } 408 | else { 409 | arg1 += (arg0 - 1); 410 | arg2 += (arg0 - 1); 411 | for (lx = 0; lx < arg0; lx++, arg1--, arg2--) { 412 | MemW1(arg2, Mem1(arg1)); 413 | } 414 | } 415 | } 416 | 417 | glsi32 OP_FTONUMZ(glui32 arg0) { 418 | gfloat32 valf = decode_float(arg0); 419 | if (!signbit(valf)) { 420 | if (isnan(valf) || isinf(valf) || (valf > 2147483647.0)) { 421 | return 0x7FFFFFFF; 422 | } else { 423 | return (glsi32) (truncf(valf)); 424 | } 425 | } else { 426 | if (isnan(valf) || isinf(valf) || (valf < -2147483647.0)) { 427 | return 0x80000000; 428 | } else { 429 | return (glsi32) (truncf(valf)); 430 | } 431 | } 432 | } 433 | 434 | glsi32 OP_FTONUMN(glui32 arg0) { 435 | gfloat32 valf = decode_float(arg0); 436 | if (!signbit(valf)) { 437 | if (isnan(valf) || isinf(valf) || (valf > 2147483647.0)) { 438 | return 0x7FFFFFFF; 439 | } else { 440 | return (glsi32) (roundf(valf)); 441 | } 442 | } else { 443 | if (isnan(valf) || isinf(valf) || (valf < -2147483647.0)) { 444 | return 0x80000000; 445 | } else { 446 | return (glsi32) (roundf(valf)); 447 | } 448 | } 449 | } 450 | 451 | void OP_FMOD(glui32 arg0, glui32 arg1, glui32 *dest0, glui32 *dest1) { 452 | gfloat32 valf1 = decode_float(arg0); 453 | gfloat32 valf2 = decode_float(arg1); 454 | gfloat32 valf = fmodf(valf1, valf2); 455 | glui32 val0 = encode_float(valf); 456 | glui32 val1 = encode_float((valf1 - valf) / valf2); 457 | if (val1 == 0x0 || val1 == 0x80000000) { 458 | /* When the quotient is zero, the sign has been lost in the 459 | shuffle. We'll set that by hand, based on the original 460 | arguments. */ 461 | val1 = (arg0 ^ arg1) & 0x80000000; 462 | } 463 | *dest0 = val0; 464 | *dest1 = val1; 465 | } 466 | 467 | glui32 OP_CEIL(glui32 arg0) { 468 | gfloat32 valf = decode_float(arg0); 469 | glui32 value = encode_float(ceilf(valf)); 470 | if (value == 0x0 || value == 0x80000000) { 471 | /* When the result is zero, the sign may have been lost in the 472 | shuffle. (This is a bug in some C libraries.) We'll set the 473 | sign by hand, based on the original argument. */ 474 | value = arg0 & 0x80000000; 475 | } 476 | return value; 477 | } 478 | 479 | glui32 OP_JFEQ(glui32 arg0, glui32 arg1, glui32 arg2) { 480 | if ((arg2 & 0x7F800000) == 0x7F800000 && (arg2 & 0x007FFFFF) != 0) { 481 | /* The delta is NaN, which can never match. */ 482 | return 0; 483 | } else if ((arg0 == 0x7F800000 || arg0 == 0xFF800000) 484 | && (arg1 == 0x7F800000 || arg1 == 0xFF800000)) { 485 | /* Both are infinite. Opposite infinities are never equal, 486 | even if the difference is infinite, so this is easy. */ 487 | return (arg0 == arg1); 488 | } else { 489 | gfloat32 valf1 = decode_float(arg1) - decode_float(arg0); 490 | gfloat32 valf2 = fabs(decode_float(arg2)); 491 | return (valf1 <= valf2 && valf1 >= -valf2); 492 | } 493 | } 494 | 495 | glui32 PopStack(void) { 496 | if (stackptr < valstackbase + 4) { 497 | fatal_error("Stack underflow in operand."); 498 | } 499 | stackptr -= 4; 500 | return Stk4(stackptr); 501 | } 502 | 503 | void PushStack(glui32 storeval) { 504 | if (stackptr + 4 > stacksize) { 505 | fatal_error("Stack overflow in store operand."); 506 | } 507 | StkW4(stackptr, storeval); 508 | stackptr += 4; 509 | } 510 | 511 | int VM_BRANCH(glui32 offset, glui32 next) { 512 | if (offset == 0 || offset == 1) 513 | { 514 | leave_function(); 515 | if (stackptr == 0) 516 | { 517 | return 1; 518 | } 519 | pop_callstub(offset); 520 | } else { 521 | pc = next + offset - 2; 522 | } 523 | return 0; 524 | } 525 | 526 | int VM_CALL_FUNCTION(glui32 addr, glui32 count, glui32 storetype, glui32 storeval, glui32 next) { 527 | if (VM_FUNC_IS_SAFE(addr)) { 528 | glui32 result, oldsp, oldvsb, res; 529 | result = CALL_FUNC(VM_CALL_SAFE_FUNCTION_WITH_STACK_ARGS(addr, count), count); 530 | store_operand(storetype, storeval, result); 531 | return 0; 532 | } 533 | else { 534 | glui32 *arglist; 535 | arglist = pop_arguments(count, 0); 536 | pc = next; 537 | push_callstub(storetype, storeval); 538 | enter_function(addr, count, arglist); 539 | return 1; 540 | } 541 | } 542 | 543 | // Try to recover from an invalid unsafe PC by seeing if we can call a safe function 544 | int VM_JUMP_CALL(glui32 pc) { 545 | // The PC we've been given is the beginning of a function's code 546 | // The header is variable length though, so call a helper function to find the function address 547 | pc = VM_FUNC_SUBTRACT_HEADER(pc); 548 | if (VM_FUNC_IS_SAFE(pc)) { 549 | glui32 count; 550 | // Retrieve the stack count for varargs functions 551 | if (VM_FUNC_IS_SAFE_VARARGS(pc)) { 552 | count = PopStack(); 553 | } 554 | // Or push the locals in reverse order for regular functions 555 | else { 556 | glui32 locals = valstackbase - localsbase; 557 | count = locals / 4; 558 | while (locals > 0) { 559 | locals -= 4; 560 | PushStack(ReadLocal(locals)); 561 | } 562 | } 563 | VM_TAILCALL_FUNCTION(pc, count); 564 | return 1; 565 | } 566 | return 0; 567 | } 568 | 569 | void VM_TAILCALL_FUNCTION(glui32 addr, glui32 count) { 570 | if (VM_FUNC_IS_SAFE(addr)) { 571 | glui32 result, oldsp, oldvsb, res; 572 | result = CALL_FUNC(VM_CALL_SAFE_FUNCTION_WITH_STACK_ARGS(addr, count), count); 573 | leave_function(); 574 | if (stackptr != 0) { 575 | pop_callstub(result); 576 | } 577 | } 578 | else { 579 | glui32 *arglist; 580 | arglist = pop_arguments(count, 0); 581 | leave_function(); 582 | enter_function(addr, count, arglist); 583 | } 584 | } -------------------------------------------------------------------------------- /glulxtoc/src/output/templates/unixstrt.c: -------------------------------------------------------------------------------- 1 | /* unixstrt.c: Unix-specific code for Glulxe. 2 | Designed by Andrew Plotkin 3 | http://eblong.com/zarf/glulx/index.html 4 | */ 5 | 6 | #include 7 | #include 8 | #include "glk.h" 9 | #include "gi_blorb.h" 10 | #include "glulxe.h" 11 | #include "unixstrt.h" 12 | #include "glkstart.h" /* This comes with the Glk library. */ 13 | #include "glulxtoc.h" 14 | 15 | /* With glulxtoc the only argument is the number of undo states. 16 | */ 17 | glkunix_argumentlist_t glkunix_arguments[] = { 18 | { "--undo", glkunix_arg_ValueFollows, "Number of undo states to store." }, 19 | { "", glkunix_arg_ValueFollows, "filename: Ignored" }, 20 | { NULL, glkunix_arg_End, NULL } 21 | }; 22 | 23 | int glkunix_startup_code(glkunix_startup_t *data) 24 | { 25 | /* It turns out to be more convenient if we return TRUE from here, even 26 | when an error occurs, and display an error in glk_main(). */ 27 | int ix; 28 | unsigned char buf[12]; 29 | int res; 30 | 31 | /* Parse out the arguments. They've already been checked for validity, 32 | and the library-specific ones stripped out. 33 | As usual for Unix, the zeroth argument is the executable name. */ 34 | for (ix=1; ixargc; ix++) { 35 | if (!strcmp(data->argv[ix], "--undo")) { 36 | ix++; 37 | if (ixargc) { 38 | int val = atoi(data->argv[ix]); 39 | if (val <= 0) { 40 | init_err = "--undo must be a number."; 41 | return TRUE; 42 | } 43 | max_undo_level = val; 44 | } 45 | continue; 46 | } 47 | } 48 | 49 | gamefile = glk_stream_open_memory(GLULX_IMAGE, GLULX_IMAGE_LENGTH, filemode_Read, 1); 50 | if (!gamefile) { 51 | init_err = "The game file could not be opened."; 52 | return TRUE; 53 | } 54 | 55 | /* Now we have to check to see if it's a Blorb file. */ 56 | 57 | glk_stream_set_position(gamefile, 0, seekmode_Start); 58 | res = glk_get_buffer_stream(gamefile, (char *)buf, 12); 59 | if (!res) { 60 | init_err = "The data in this stand-alone game is too short to read."; 61 | return TRUE; 62 | } 63 | 64 | if (buf[0] == 'G' && buf[1] == 'l' && buf[2] == 'u' && buf[3] == 'l') { 65 | /* Load game directly from file. */ 66 | locate_gamefile(FALSE); 67 | return TRUE; 68 | } 69 | else if (buf[0] == 'F' && buf[1] == 'O' && buf[2] == 'R' && buf[3] == 'M' 70 | && buf[8] == 'I' && buf[9] == 'F' && buf[10] == 'R' && buf[11] == 'S') { 71 | /* Load game from a chunk in the Blorb file. */ 72 | locate_gamefile(TRUE); 73 | return TRUE; 74 | } 75 | else { 76 | init_err = "This is neither a Glulx game file nor a Blorb file " 77 | "which contains one."; 78 | return TRUE; 79 | } 80 | } -------------------------------------------------------------------------------- /if-decompiler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "if-decompiler" 3 | version = "0.1.0" 4 | authors = ["Dannii Willis "] 5 | edition = "2018" 6 | description = "Decompile various interactive fiction formats" 7 | homepage = "https://github.com/curiousdannii/if-decompiler" 8 | license = "MIT" 9 | repository = "https://github.com/curiousdannii/if-decompiler" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | bytes = "1.0.1" 15 | fnv = "1.0.7" 16 | petgraph = "0.6.0" -------------------------------------------------------------------------------- /if-decompiler/README.md: -------------------------------------------------------------------------------- 1 | IF-Decompiler 2 | ============= 3 | 4 | Decompile various interactive fiction formats. 5 | 6 | Currently supports: 7 | 8 | - Glulx -------------------------------------------------------------------------------- /if-decompiler/src/glulx/disassembler.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Glulx Disassembler 4 | ================== 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use fnv::FnvHashSet; 13 | 14 | use super::*; 15 | 16 | impl GlulxState { 17 | pub fn disassemble(&mut self, image: &[u8]) -> FnvHashSet<(u32, u32)> { 18 | let decoding_table = self.parse_string_decoding_table(image); 19 | 20 | let mut edges = FnvHashSet::default(); 21 | 22 | let ram_start = self.read_addr(image, 8) as u64; 23 | self.ramstart = ram_start as u32; 24 | let decoding_table_addr = self.read_addr(image, 28); 25 | let root_node_addr = self.read_addr(image, decoding_table_addr + 8); 26 | 27 | let mut cursor = Cursor::new(image); 28 | 29 | // If we have debug file data, use it to disassemble all the functions 30 | if let Some(functions) = &self.debug_function_data { 31 | for (&addr, func) in functions { 32 | cursor.set_position(addr as u64); 33 | let function_type = cursor.get_u8(); 34 | self.functions.insert(func.addr, self.disassemble_function(&mut cursor, &mut edges, addr, Some(func.len), function_type)); 35 | } 36 | return edges; 37 | } 38 | 39 | // Otherwise parse the file manually 40 | // Skip past the header 41 | cursor.set_position(60); 42 | 43 | // Loop through the ROM until the end of RAM or we find a 44 | while cursor.position() < ram_start { 45 | let addr = cursor.position() as u32; 46 | let object_type = cursor.get_u8(); 47 | 48 | match object_type { 49 | // Padding 50 | 0 => {}, 51 | 52 | // Functions 53 | 0xC0 | 0xC1 => { 54 | self.functions.insert(addr, self.disassemble_function(&mut cursor, &mut edges, addr, None, object_type)); 55 | }, 56 | 57 | // Strings - just skip past them for now! 58 | 0xE0 => { 59 | if self.stop_on_string { 60 | break; 61 | } 62 | while cursor.get_u8() != 0 {} 63 | }, 64 | 0xE2 => { 65 | if self.stop_on_string { 66 | break; 67 | } 68 | cursor.get_u8(); 69 | cursor.get_u8(); 70 | cursor.get_u8(); 71 | while cursor.get_u32() != 0 {} 72 | }, 73 | // Compressed strings will take a bit more work... 74 | 0xE1 => { 75 | if self.stop_on_string { 76 | break; 77 | } 78 | 79 | fn get_node<'a>(table: &'a FnvHashMap, addr: u32) -> &'a DecodingNode { 80 | table.get(&addr).unwrap() 81 | } 82 | fn get_node_branch_addresses(node: &DecodingNode) -> [u32; 2] { 83 | match node { 84 | DecodingNode::Branch(branch) => { 85 | [branch.left, branch.right] 86 | }, 87 | _ => panic!("Decoding node is not a branch"), 88 | } 89 | } 90 | 91 | let root_node = get_node(&decoding_table, root_node_addr); 92 | let root_branches = get_node_branch_addresses(root_node); 93 | let mut left_node = root_branches[0]; 94 | let mut right_node = root_branches[1]; 95 | 96 | let mut byte = cursor.get_u8(); 97 | let mut bits = 8; 98 | loop { 99 | let bit = byte & 0x01; 100 | bits -= 1; 101 | byte >>= 1; 102 | let node = get_node(&decoding_table, if bit == 0 {left_node} else {right_node}); 103 | match node { 104 | DecodingNode::Terminator => { 105 | break; 106 | }, 107 | DecodingNode::Leaf => { 108 | left_node = root_branches[0]; 109 | right_node = root_branches[1]; 110 | }, 111 | DecodingNode::Branch(branch) => { 112 | left_node = branch.left; 113 | right_node = branch.right; 114 | }, 115 | } 116 | if bits == 0 { 117 | bits = 8; 118 | byte = cursor.get_u8(); 119 | } 120 | } 121 | }, 122 | 123 | // Unknown 124 | _ => { 125 | println!("Stopping on unknown object type {:?} at {:?}", object_type, addr); 126 | break; 127 | }, 128 | } 129 | }; 130 | 131 | // Return the list of edges 132 | edges 133 | } 134 | 135 | // Parse the string decoding table, but only so that we can ignore compressed strings 136 | pub fn parse_string_decoding_table(&self, image: &[u8]) -> FnvHashMap { 137 | let mut table = FnvHashMap::default(); 138 | let mut cursor = Cursor::new(image); 139 | 140 | let decoding_table_addr = self.read_addr(image, 28); 141 | let root_node_addr = self.read_addr(image, decoding_table_addr + 8); 142 | 143 | // Keep a list of nodes to process and loop through 144 | // I tried doing this recursively but couldn't make it work with the borrow checker 145 | let mut nodes_to_process = vec![root_node_addr]; 146 | loop { 147 | let addr = nodes_to_process.pop().unwrap(); 148 | cursor.set_position(addr as u64); 149 | let node_type = cursor.get_u8(); 150 | let node = match node_type { 151 | 0x00 => { 152 | let left = cursor.get_u32(); 153 | let right = cursor.get_u32(); 154 | nodes_to_process.push(left); 155 | nodes_to_process.push(right); 156 | DecodingNode::Branch(DecodingNodeBranch { 157 | left, 158 | right, 159 | }) 160 | }, 161 | 0x01 => DecodingNode::Terminator, 162 | 0x02 => { 163 | cursor.get_u8(); 164 | DecodingNode::Leaf 165 | }, 166 | 0x03 => { 167 | while cursor.get_u8() != 0 {} 168 | DecodingNode::Leaf 169 | }, 170 | 0x04 | 0x08 | 0x09 => { 171 | cursor.get_u32(); 172 | DecodingNode::Leaf 173 | }, 174 | 0x05 => { 175 | while cursor.get_u32() != 0 {} 176 | DecodingNode::Leaf 177 | }, 178 | 0x0A | 0x0B => { 179 | let _addr = cursor.get_u32(); 180 | let count = cursor.get_u32(); 181 | for _ in 0..count { 182 | cursor.get_u32(); 183 | } 184 | DecodingNode::Leaf 185 | } 186 | _ => panic!("Invalid string decoding node at {}", addr), 187 | }; 188 | table.insert(addr, node); 189 | if nodes_to_process.len() == 0 { 190 | break; 191 | } 192 | } 193 | 194 | table 195 | } 196 | 197 | fn disassemble_function(&self, cursor: &mut Cursor<&[u8]>, edges: &mut FnvHashSet<(u32, u32)>, addr: u32, len: Option, function_mode: u8) -> Function { 198 | let argument_mode = match function_mode { 199 | 0xC0 => FunctionArgumentMode::Stack, 200 | 0xC1 => FunctionArgumentMode::Locals, 201 | _ => unreachable!(), 202 | }; 203 | 204 | // Parse the locals formats 205 | let mut locals = 0; 206 | loop { 207 | let local_type = cursor.get_u8(); 208 | let count = cursor.get_u8() as u32; 209 | if local_type == 0 { 210 | break 211 | } 212 | if local_type != 4 { 213 | panic!("1 and 2 byte locals are not supported in function {}", addr); 214 | } 215 | locals += count; 216 | } 217 | 218 | // Basic blocks 219 | let mut entry_points = FnvHashSet::default(); 220 | let mut exit_branches = FnvHashMap::default(); 221 | 222 | // Parse the instructions 223 | let end_addr = len.map(|l| addr + l); 224 | let mut instructions = Vec::new(); 225 | let mut instruction_addresses = FnvHashSet::default(); 226 | 'parse_loop: loop { 227 | let instruction = self.disassemble_instruction(cursor); 228 | instruction_addresses.insert(instruction.addr); 229 | 230 | // If this instruction branches, then update the entry and exit points 231 | if let Some(target) = instruction.branch { 232 | match instruction.opcode { 233 | opcodes::OP_JUMP | opcodes::OP_JUMPABS => { 234 | let mut branch_targets = Vec::new(); 235 | if let BranchTarget::Absolute(addr) = target { 236 | entry_points.insert(addr); 237 | branch_targets.push(addr); 238 | } 239 | exit_branches.insert(instruction.addr, branch_targets); 240 | }, 241 | _ => { 242 | // If the branch returns then don't end a basic block here 243 | // Except for @catch! 244 | let returns = match target { 245 | BranchTarget::Return(_) => true, 246 | _ => false, 247 | }; 248 | if !returns || instruction.opcode == opcodes::OP_CATCH { 249 | entry_points.insert(instruction.next); 250 | let mut branch_targets = vec![instruction.next]; 251 | if let BranchTarget::Absolute(addr) = target { 252 | entry_points.insert(addr); 253 | branch_targets.push(addr); 254 | } 255 | exit_branches.insert(instruction.addr, branch_targets); 256 | } 257 | }, 258 | }; 259 | } 260 | let opcode = instruction.opcode; 261 | 262 | // If this instruction calls, then add it to the edges list 263 | if opcodes::instruction_calls(opcode) { 264 | if let Operand::Constant(callee_addr) = instruction.operands[0] { 265 | edges.insert((addr, callee_addr)); 266 | } 267 | } 268 | 269 | // Add an entry point for instructions which may resume later 270 | if opcodes::instruction_resumes(opcode) { 271 | entry_points.insert(instruction.next); 272 | } 273 | 274 | instructions.push(instruction); 275 | 276 | // If we have an end_addr (from a debug file) then use it to determine when to stop decoding 277 | if let Some(end_addr) = end_addr { 278 | if cursor.position() as u32 == end_addr { 279 | break; 280 | } 281 | continue; 282 | } 283 | 284 | if opcodes::instruction_halts(opcode) { 285 | // Stop parsing instructions if we don't have any pending entry_points 286 | // Short cut - check if the next address is an entry point 287 | if !entry_points.contains(&(cursor.position() as u32)) { 288 | // Otherwise check if any entry points haven't already been parsed 289 | for _ in entry_points.difference(&instruction_addresses) { 290 | continue 'parse_loop; 291 | } 292 | 293 | // And check for an unreachable instruction 294 | let final_addr = cursor.position(); 295 | let potential_opcode = decode_opcode(cursor); 296 | cursor.set_position(final_addr); 297 | // Check for 0 first, as it shouldn't be interpreted as a NOP 298 | if potential_opcode == 0 { 299 | break; 300 | } 301 | match opcodes::operands_count(potential_opcode) { 302 | Some(_) => { 303 | entry_points.insert(final_addr as u32); 304 | continue 'parse_loop; 305 | }, 306 | None => break, 307 | }; 308 | } 309 | } 310 | } 311 | 312 | let safety = self.function_safety(addr, &instructions); 313 | let blocks = calculate_basic_blocks(instructions, entry_points, exit_branches); 314 | 315 | Function { 316 | addr, 317 | argument_mode, 318 | blocks, 319 | locals, 320 | safety, 321 | } 322 | } 323 | 324 | fn disassemble_instruction(&self, cursor: &mut Cursor<&[u8]>) -> Instruction { 325 | use Operand::*; 326 | 327 | let addr = cursor.position() as u32; 328 | let opcode = decode_opcode(cursor); 329 | 330 | // Extract the operands 331 | let mut operands = Vec::default(); 332 | let operands_count = opcodes::operands_count(opcode).expect(&format!("Unknown opcode {} at address {}", opcode, addr)) as usize; 333 | let mut operand_types = Vec::default(); 334 | while operand_types.len() < operands_count { 335 | let types = cursor.get_u8(); 336 | operand_types.push(types & 0x0F); 337 | operand_types.push(types >> 4); 338 | } 339 | for i in 0..operands_count { 340 | let operand = match operand_types[i] { 341 | 0 => Constant(0), 342 | 1 => Constant(cursor.get_i8() as i32 as u32), 343 | 2 => Constant(cursor.get_i16() as i32 as u32), 344 | 3 => Constant(cursor.get_u32()), 345 | 5 => Memory(cursor.get_u8() as u32), 346 | 6 => Memory(cursor.get_u16() as u32), 347 | 7 => Memory(cursor.get_u32()), 348 | 8 => Stack, 349 | 9 => Local(cursor.get_u8() as u32), 350 | 10 => Local(cursor.get_u16() as u32), 351 | 11 => Local(cursor.get_u32()), 352 | 13 => RAM(cursor.get_u8() as u32), 353 | 14 => RAM(cursor.get_u16() as u32), 354 | 15 => RAM(cursor.get_u32()), 355 | x => panic!("Invalid operand mode {} in instruction {}", x, addr), 356 | }; 357 | operands.push(operand); 358 | } 359 | 360 | // Calculate branch targets 361 | use BranchTarget::*; 362 | let calc_branch = || -> BranchTarget { 363 | match *operands.last().unwrap() { 364 | Constant(target) => { 365 | if opcode == opcodes::OP_JUMPABS { 366 | Absolute(target) 367 | } 368 | else { 369 | if target == 0 || target == 1 { 370 | Return(target) 371 | } 372 | else { 373 | Absolute((cursor.position() as i32 + target as i32 - 2) as u32) 374 | } 375 | } 376 | }, 377 | _ => Dynamic, 378 | } 379 | }; 380 | let branch = match opcodes::instruction_branches(opcode) { 381 | true => Some(calc_branch()), 382 | false => None, 383 | }; 384 | 385 | // Extract the storer(s) - in reverse order (makes it simpler for OP_FMOD) 386 | use opcodes::StoreMode::*; 387 | let (storer2, storer) = match opcodes::instruction_stores(opcode) { 388 | DoesNotStore => (Operand::Constant(0), Operand::Constant(0)), 389 | LastOperand => (Operand::Constant(0), operands.pop().unwrap()), 390 | LastTwoOperands => (operands.pop().unwrap(), operands.pop().unwrap()), 391 | }; 392 | 393 | Instruction { 394 | addr, 395 | opcode, 396 | operands, 397 | branch, 398 | storer, 399 | storer2, 400 | next: cursor.position() as u32, 401 | } 402 | } 403 | 404 | // Check the function safety overrides 405 | fn function_safety(&self, addr: u32, instructions: &Vec) -> FunctionSafety { 406 | if let Some(functions) = &self.safe_function_overides { 407 | if functions.contains(&addr) { 408 | return FunctionSafety::SafetyTBD; 409 | } 410 | } 411 | if let Some(functions) = &self.unsafe_function_overides { 412 | if functions.contains(&addr) { 413 | return FunctionSafety::Unsafe; 414 | } 415 | } 416 | opcodes::function_safety(instructions) 417 | } 418 | } 419 | 420 | // Decode a variable length opcode 421 | fn decode_opcode(cursor: &mut Cursor<&[u8]>) -> u32 { 422 | let opcode_byte = cursor.get_u8(); 423 | match opcode_byte { 424 | 0 ..= 0x7F => opcode_byte as u32, 425 | 0x80 ..= 0xBF => ((opcode_byte as u32 & 0x3F) << 8) | cursor.get_u8() as u32, 426 | 0xC0 ..= 0xFF => ((opcode_byte as u32 & 0x3F) << 24) | ((cursor.get_u8() as u32) << 16) | cursor.get_u16() as u32, 427 | } 428 | } 429 | 430 | pub enum DecodingNode { 431 | Branch(DecodingNodeBranch), 432 | Leaf, 433 | Terminator, 434 | } 435 | 436 | pub struct DecodingNodeBranch { 437 | pub left: u32, 438 | pub right: u32, 439 | } -------------------------------------------------------------------------------- /if-decompiler/src/glulx/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Glulx 4 | ===== 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use std::collections::BTreeMap; 13 | use std::io::Cursor; 14 | 15 | use bytes::Buf; 16 | 17 | use super::*; 18 | 19 | mod disassembler; 20 | pub mod opcodes; 21 | 22 | pub struct GlulxState { 23 | pub debug_function_data: Option>, 24 | pub functions: BTreeMap, 25 | pub ramstart: u32, 26 | pub safe_function_overides: Option>, 27 | pub stop_on_string: bool, 28 | pub unsafe_function_overides: Option>, 29 | } 30 | 31 | impl GlulxState { 32 | pub fn new(debug_function_data: Option>, safe_function_overides: Option>, stop_on_string: bool, unsafe_function_overides: Option>) -> Self { 33 | GlulxState { 34 | debug_function_data, 35 | functions: BTreeMap::default(), 36 | ramstart: 0, 37 | safe_function_overides, 38 | stop_on_string, 39 | unsafe_function_overides, 40 | } 41 | } 42 | 43 | pub fn decompile_rom(&mut self, image: &[u8]) { 44 | let edges = self.disassemble(image); 45 | self.mark_all_unsafe_functions(edges); 46 | } 47 | 48 | pub fn read_addr(&self, image: &[u8], addr: u32) -> u32 { 49 | let mut cursor = Cursor::new(image); 50 | cursor.set_position(addr as u64); 51 | cursor.get_u32() 52 | } 53 | } 54 | 55 | impl VirtualMachine for GlulxState { 56 | fn get_functions(&self) -> FnvHashMap { 57 | let mut res = FnvHashMap::default(); 58 | for (&addr, function) in &self.functions { 59 | res.insert(addr, function.safety); 60 | } 61 | res 62 | } 63 | 64 | fn mark_function_as_unsafe(&mut self, addr: u32) { 65 | let function = self.functions.get_mut(&addr).unwrap(); 66 | if function.safety == FunctionSafety::SafetyTBD { 67 | function.safety = FunctionSafety::Unsafe; 68 | } 69 | } 70 | } 71 | 72 | pub struct Function { 73 | pub addr: u32, 74 | pub argument_mode: FunctionArgumentMode, 75 | pub blocks: BTreeMap>, 76 | pub locals: u32, 77 | pub safety: FunctionSafety, 78 | } 79 | 80 | #[derive(Copy, Clone, Debug, PartialEq)] 81 | pub enum FunctionArgumentMode { 82 | Stack, 83 | Locals, 84 | } 85 | 86 | pub struct Instruction { 87 | pub addr: u32, 88 | pub opcode: u32, 89 | pub operands: Vec, 90 | pub branch: Option, 91 | // These could be inside an Option, but we can just set them to Constants if the instruction doesn't store 92 | pub storer: Operand, 93 | pub storer2: Operand, 94 | pub next: u32, 95 | } 96 | 97 | impl VMInstruction for Instruction { 98 | fn addr(&self) -> u32 { 99 | self.addr 100 | } 101 | 102 | fn does_halt(&self) -> bool { 103 | opcodes::instruction_halts(self.opcode) 104 | } 105 | } 106 | 107 | #[derive(Copy, Clone, Debug)] 108 | pub enum Operand { 109 | Constant(u32), 110 | Memory(u32), 111 | Stack, 112 | Local(u32), 113 | RAM(u32), 114 | } -------------------------------------------------------------------------------- /if-decompiler/src/glulx/opcodes.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Glulx Opcodes 4 | ============= 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use super::*; 13 | 14 | pub const OP_NOP: u32 = 0x00; 15 | pub const OP_ADD: u32 = 0x10; 16 | pub const OP_SUB: u32 = 0x11; 17 | pub const OP_MUL: u32 = 0x12; 18 | pub const OP_DIV: u32 = 0x13; 19 | pub const OP_MOD: u32 = 0x14; 20 | pub const OP_NEG: u32 = 0x15; 21 | pub const OP_BITAND: u32 = 0x18; 22 | pub const OP_BITOR: u32 = 0x19; 23 | pub const OP_BITXOR: u32 = 0x1A; 24 | pub const OP_BITNOT: u32 = 0x1B; 25 | pub const OP_SHIFTL: u32 = 0x1C; 26 | pub const OP_SSHIFTR: u32 = 0x1D; 27 | pub const OP_USHIFTR: u32 = 0x1E; 28 | pub const OP_JUMP: u32 = 0x20; 29 | pub const OP_JZ: u32 = 0x22; 30 | pub const OP_JNZ: u32 = 0x23; 31 | pub const OP_JEQ: u32 = 0x24; 32 | pub const OP_JNE: u32 = 0x25; 33 | pub const OP_JLT: u32 = 0x26; 34 | pub const OP_JGE: u32 = 0x27; 35 | pub const OP_JGT: u32 = 0x28; 36 | pub const OP_JLE: u32 = 0x29; 37 | pub const OP_JLTU: u32 = 0x2A; 38 | pub const OP_JGEU: u32 = 0x2B; 39 | pub const OP_JGTU: u32 = 0x2C; 40 | pub const OP_JLEU: u32 = 0x2D; 41 | pub const OP_CALL: u32 = 0x30; 42 | pub const OP_RETURN: u32 = 0x31; 43 | pub const OP_CATCH: u32 = 0x32; 44 | pub const OP_THROW: u32 = 0x33; 45 | pub const OP_TAILCALL: u32 = 0x34; 46 | pub const OP_COPY: u32 = 0x40; 47 | pub const OP_COPYS: u32 = 0x41; 48 | pub const OP_COPYB: u32 = 0x42; 49 | pub const OP_SEXS: u32 = 0x44; 50 | pub const OP_SEXB: u32 = 0x45; 51 | pub const OP_ALOAD: u32 = 0x48; 52 | pub const OP_ALOADS: u32 = 0x49; 53 | pub const OP_ALOADB: u32 = 0x4A; 54 | pub const OP_ALOADBIT: u32 = 0x4B; 55 | pub const OP_ASTORE: u32 = 0x4C; 56 | pub const OP_ASTORES: u32 = 0x4D; 57 | pub const OP_ASTOREB: u32 = 0x4E; 58 | pub const OP_ASTOREBIT: u32 = 0x4F; 59 | pub const OP_STKCOUNT: u32 = 0x50; 60 | pub const OP_STKPEEK: u32 = 0x51; 61 | pub const OP_STKSWAP: u32 = 0x52; 62 | pub const OP_STKROLL: u32 = 0x53; 63 | pub const OP_STKCOPY: u32 = 0x54; 64 | pub const OP_STREAMCHAR: u32 = 0x70; 65 | pub const OP_STREAMNUM: u32 = 0x71; 66 | pub const OP_STREAMSTR: u32 = 0x72; 67 | pub const OP_STREAMUNICHAR: u32 = 0x73; 68 | pub const OP_GESTALT: u32 = 0x100; 69 | pub const OP_DEBUGTRAP: u32 = 0x101; 70 | pub const OP_GETMEMSIZE: u32 = 0x102; 71 | pub const OP_SETMEMSIZE: u32 = 0x103; 72 | pub const OP_JUMPABS: u32 = 0x104; 73 | pub const OP_RANDOM: u32 = 0x110; 74 | pub const OP_SETRANDOM: u32 = 0x111; 75 | pub const OP_QUIT: u32 = 0x120; 76 | pub const OP_VERIFY: u32 = 0x121; 77 | pub const OP_RESTART: u32 = 0x122; 78 | pub const OP_SAVE: u32 = 0x123; 79 | pub const OP_RESTORE: u32 = 0x124; 80 | pub const OP_SAVEUNDO: u32 = 0x125; 81 | pub const OP_RESTOREUNDO: u32 = 0x126; 82 | pub const OP_PROTECT: u32 = 0x127; 83 | pub const OP_GLK: u32 = 0x130; 84 | pub const OP_GETSTRINGTBL: u32 = 0x140; 85 | pub const OP_SETSTRINGTBL: u32 = 0x141; 86 | pub const OP_GETIOSYS: u32 = 0x148; 87 | pub const OP_SETIOSYS: u32 = 0x149; 88 | pub const OP_LINEARSEARCH: u32 = 0x150; 89 | pub const OP_BINARYSEARCH: u32 = 0x151; 90 | pub const OP_LINKEDSEARCH: u32 = 0x152; 91 | pub const OP_CALLF: u32 = 0x160; 92 | pub const OP_CALLFI: u32 = 0x161; 93 | pub const OP_CALLFII: u32 = 0x162; 94 | pub const OP_CALLFIII: u32 = 0x163; 95 | pub const OP_MZERO: u32 = 0x170; 96 | pub const OP_MCOPY: u32 = 0x171; 97 | pub const OP_MALLOC: u32 = 0x178; 98 | pub const OP_MFREE: u32 = 0x179; 99 | pub const OP_ACCELFUNC: u32 = 0x180; 100 | pub const OP_ACCELPARAM: u32 = 0x181; 101 | pub const OP_NUMTOF: u32 = 0x190; 102 | pub const OP_FTONUMZ: u32 = 0x191; 103 | pub const OP_FTONUMN: u32 = 0x192; 104 | pub const OP_CEIL: u32 = 0x198; 105 | pub const OP_FLOOR: u32 = 0x199; 106 | pub const OP_FADD: u32 = 0x1A0; 107 | pub const OP_FSUB: u32 = 0x1A1; 108 | pub const OP_FMUL: u32 = 0x1A2; 109 | pub const OP_FDIV: u32 = 0x1A3; 110 | pub const OP_FMOD: u32 = 0x1A4; 111 | pub const OP_SQRT: u32 = 0x1A8; 112 | pub const OP_EXP: u32 = 0x1A9; 113 | pub const OP_LOG: u32 = 0x1AA; 114 | pub const OP_POW: u32 = 0x1AB; 115 | pub const OP_SIN: u32 = 0x1B0; 116 | pub const OP_COS: u32 = 0x1B1; 117 | pub const OP_TAN: u32 = 0x1B2; 118 | pub const OP_ASIN: u32 = 0x1B3; 119 | pub const OP_ACOS: u32 = 0x1B4; 120 | pub const OP_ATAN: u32 = 0x1B5; 121 | pub const OP_ATAN2: u32 = 0x1B6; 122 | pub const OP_JFEQ: u32 = 0x1C0; 123 | pub const OP_JFNE: u32 = 0x1C1; 124 | pub const OP_JFLT: u32 = 0x1C2; 125 | pub const OP_JFLE: u32 = 0x1C3; 126 | pub const OP_JFGT: u32 = 0x1C4; 127 | pub const OP_JFGE: u32 = 0x1C5; 128 | pub const OP_JISNAN: u32 = 0x1C8; 129 | pub const OP_JISINF: u32 = 0x1C9; 130 | 131 | // Return the number of operands an opcode has 132 | // Also checks for unknown opcodes 133 | pub fn operands_count(opcode: u32) -> Option { 134 | match opcode { 135 | OP_NOP | OP_STKSWAP | OP_QUIT | OP_RESTART => Some(0), 136 | OP_JUMP | OP_RETURN | OP_STKCOUNT | OP_STKCOPY 137 | | OP_STREAMCHAR ..= OP_STREAMUNICHAR | OP_DEBUGTRAP | OP_GETMEMSIZE 138 | | OP_JUMPABS | OP_SETRANDOM | OP_VERIFY | OP_SAVEUNDO | OP_RESTOREUNDO 139 | | OP_GETSTRINGTBL | OP_SETSTRINGTBL | OP_MFREE => Some(1), 140 | OP_NEG | OP_BITNOT | OP_JZ | OP_JNZ | OP_CATCH ..= OP_TAILCALL 141 | | OP_COPY ..= OP_SEXB | OP_STKPEEK | OP_STKROLL | OP_CALLF 142 | | OP_SETMEMSIZE | OP_RANDOM | OP_SAVE | OP_RESTORE | OP_PROTECT 143 | | OP_GETIOSYS | OP_SETIOSYS | OP_MZERO | OP_MALLOC | OP_ACCELFUNC 144 | | OP_ACCELPARAM | OP_NUMTOF ..= OP_FLOOR | OP_SQRT ..= OP_LOG 145 | | OP_SIN ..= OP_ATAN | OP_JISNAN | OP_JISINF => Some(2), 146 | OP_ADD ..= OP_MOD | OP_BITAND ..= OP_BITXOR | OP_SHIFTL ..= OP_USHIFTR 147 | | OP_JEQ ..= OP_CALL | OP_ALOAD ..= OP_ASTOREBIT | OP_GESTALT 148 | | OP_GLK | OP_CALLFI | OP_MCOPY | OP_FADD ..= OP_FDIV | OP_POW 149 | | OP_ATAN2 | OP_JFLT ..= OP_JFGE => Some(3), 150 | OP_CALLFII | OP_FMOD | OP_JFEQ | OP_JFNE => Some(4), 151 | OP_CALLFIII => Some(5), 152 | OP_LINKEDSEARCH => Some(7), 153 | OP_LINEARSEARCH | OP_BINARYSEARCH => Some(8), 154 | _ => None, 155 | } 156 | } 157 | 158 | // Whether an instruction branches or jumps 159 | pub fn instruction_branches(opcode: u32) -> bool { 160 | match opcode { 161 | OP_JUMP ..= OP_JLEU | OP_CATCH | OP_JUMPABS | OP_JFEQ ..= OP_JISINF => true, 162 | _ => false, 163 | } 164 | } 165 | 166 | // Whether an instruction calls 167 | pub fn instruction_calls(opcode: u32) -> bool { 168 | match opcode { 169 | OP_CALL | OP_TAILCALL | OP_CALLF ..= OP_CALLFIII => true, 170 | _ => false, 171 | } 172 | } 173 | 174 | // Whether an instruction halts rather than continuing at the next byte 175 | pub fn instruction_halts(opcode: u32) -> bool { 176 | match opcode { 177 | OP_JUMP | OP_RETURN | OP_THROW | OP_TAILCALL | OP_JUMPABS | OP_QUIT 178 | | OP_RESTART => true, 179 | _ => false, 180 | } 181 | } 182 | 183 | // Some instructions may cause execution to resume at the next instruction at some later time 184 | pub fn instruction_resumes(opcode: u32) -> bool { 185 | match opcode { 186 | OP_CALL | OP_STREAMCHAR ..= OP_STREAMUNICHAR | OP_SAVE | OP_SAVEUNDO 187 | | OP_CALLF ..= OP_CALLFIII => true, 188 | _ => false, 189 | } 190 | } 191 | 192 | #[derive(Copy, Clone, PartialEq)] 193 | pub enum StoreMode { 194 | DoesNotStore, 195 | LastOperand, 196 | LastTwoOperands, 197 | } 198 | 199 | // Whether an instruction stores 200 | pub fn instruction_stores(opcode: u32) -> StoreMode { 201 | use StoreMode::*; 202 | match opcode { 203 | OP_ADD ..= OP_USHIFTR | OP_CALL | OP_COPY 204 | /* OP_COPYS | OP_COPYB store manually */ 205 | | OP_SEXS ..= OP_ALOADBIT | OP_STKCOUNT ..= OP_STKPEEK 206 | | OP_GESTALT | OP_GETMEMSIZE | OP_SETMEMSIZE | OP_RANDOM | OP_VERIFY 207 | | OP_SAVE ..= OP_RESTOREUNDO | OP_GLK | OP_GETSTRINGTBL 208 | | OP_LINEARSEARCH ..= OP_CALLFIII | OP_MALLOC | OP_NUMTOF ..= OP_FDIV 209 | | OP_SQRT ..= OP_ATAN2 => LastOperand, 210 | OP_GETIOSYS | OP_FMOD => LastTwoOperands, 211 | _ => DoesNotStore, 212 | } 213 | } 214 | 215 | // Return the FunctionSafety for a function's instructions 216 | pub fn function_safety(instructions: &Vec) -> FunctionSafety { 217 | use FunctionSafety::*; 218 | let mut result = SafetyTBD; 219 | for instruction in instructions { 220 | match instruction.opcode { 221 | OP_THROW | OP_QUIT | OP_RESTART ..= OP_RESTOREUNDO => result = Unsafe, 222 | 223 | // Calls to non-constants are unsafe 224 | OP_CALL | OP_TAILCALL | OP_CALLF ..= OP_CALLFIII => match instruction.operands[0] { 225 | Operand::Constant(_) => continue, 226 | _ => result = Unsafe, 227 | } 228 | 229 | // Branches to non-constants are unsafe 230 | OP_JUMP ..= OP_JLEU | OP_JUMPABS | OP_JFEQ ..= OP_JISINF => match instruction.operands.last().unwrap() { 231 | Operand::Constant(_) => continue, 232 | _ => return UnsafeDynamicBranches, 233 | } 234 | 235 | // CATCH could be unsafe or extra unsafe 236 | OP_CATCH => match instruction.operands.last().unwrap() { 237 | Operand::Constant(_) => result = Unsafe, 238 | _ => return UnsafeDynamicBranches, 239 | } 240 | 241 | _ => continue, 242 | }; 243 | } 244 | result 245 | } -------------------------------------------------------------------------------- /if-decompiler/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | if-decompiler - core library 4 | =============================== 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | #![forbid(unsafe_code)] 13 | 14 | use std::collections::BTreeMap; 15 | 16 | use fnv::{FnvHashMap, FnvHashSet}; 17 | use petgraph::{graph, visit}; 18 | 19 | pub mod glulx; 20 | 21 | // Function data from an Inform debug file 22 | #[derive(Debug)] 23 | pub struct DebugFunctionData { 24 | pub addr: u32, 25 | pub len: u32, 26 | pub name: String, 27 | } 28 | 29 | // Function safety refers to whether or not a function can be compiled and run without worrying about its locals and stack being part of the savestate 30 | // Unsafe functions need to be compiled such that they can be serialised and restored 31 | // UnsafeDynamicBranches functions have dynamic branches and need even more special care 32 | // SafetyTBD functions have not yet been determined. At the end of decompilation any remaining SafetyTBD functions will be assumed safe. 33 | #[derive(Copy, Clone, Debug, PartialEq)] 34 | pub enum FunctionSafety { 35 | Unsafe, 36 | UnsafeDynamicBranches, 37 | SafetyTBD, 38 | } 39 | 40 | // Now a trait for generalising over our VMs 41 | pub trait VirtualMachine { 42 | fn get_functions(&self) -> FnvHashMap; 43 | fn mark_function_as_unsafe(&mut self, addr: u32); 44 | 45 | fn mark_all_unsafe_functions(&mut self, edges: FnvHashSet<(u32, u32)>) { 46 | let mut graph: graph::Graph = graph::Graph::new(); 47 | 48 | // Add the graph nodes 49 | let functions = self.get_functions(); 50 | let mut function_nodes = FnvHashMap::default(); 51 | let mut unsafe_functions = Vec::new(); 52 | for (addr, safety) in functions { 53 | let node = graph.add_node(addr); 54 | function_nodes.insert(addr, node); 55 | if safety != FunctionSafety::SafetyTBD { 56 | unsafe_functions.push(node); 57 | } 58 | } 59 | 60 | // Then add the graph edges 61 | graph.extend_with_edges(edges.iter().map(|(caller_addr, callee_addr)| { 62 | let caller_node = function_nodes[caller_addr]; 63 | let callee_node = function_nodes[callee_addr]; 64 | // The direction must be callee->caller, as we'll change the caller's safety if the callee is unsafe 65 | (callee_node, caller_node) 66 | })); 67 | 68 | // Now walk the function graph, marking each caller as Unsafe 69 | let mut dfs = visit::Dfs::empty(&graph); 70 | dfs.stack = unsafe_functions; 71 | while let Some(node_index) = dfs.next(&graph) { 72 | let addr = graph[node_index]; 73 | self.mark_function_as_unsafe(addr); 74 | } 75 | } 76 | } 77 | 78 | // Generic instruction functions 79 | pub trait VMInstruction { 80 | fn addr(&self) -> u32; 81 | fn does_halt(&self) -> bool; 82 | } 83 | 84 | // A generic basic block 85 | pub struct BasicBlock { 86 | pub label: u32, 87 | pub code: Vec, 88 | pub branches: FnvHashSet, 89 | } 90 | 91 | // Calculate basic blocks 92 | pub fn calculate_basic_blocks(instructions: Vec, entry_points: FnvHashSet, exit_branches: FnvHashMap>) -> BTreeMap> { 93 | let mut blocks: BTreeMap> = BTreeMap::new(); 94 | let mut current_block_addr = 0; 95 | let mut last_instruction_halted = false; 96 | for instruction in instructions { 97 | let addr = instruction.addr(); 98 | // If we're in the middle of a block, see if we should add to it 99 | if current_block_addr > 0 { 100 | let current_block = blocks.get_mut(¤t_block_addr).unwrap(); 101 | // Finish a previous block because this one starts a new one 102 | if entry_points.contains(&addr) { 103 | // Unless the last instruction halted, add this new instruction as a branch to the last block 104 | if !last_instruction_halted { 105 | current_block.branches.insert(addr); 106 | } 107 | // Make a new block below 108 | } 109 | else { 110 | // If this instruction branches, finish up the block 111 | if let Some(branches) = exit_branches.get(&addr) { 112 | for branch in branches { 113 | current_block.branches.insert(*branch); 114 | } 115 | current_block_addr = 0; 116 | } 117 | // Add to the current block 118 | last_instruction_halted = instruction.does_halt(); 119 | current_block.code.push(instruction); 120 | // Continue so we don't make a new block 121 | continue; 122 | } 123 | } 124 | // Make a new block 125 | current_block_addr = addr; 126 | last_instruction_halted = instruction.does_halt(); 127 | let mut current_block = BasicBlock:: { 128 | label: addr, 129 | code: vec![instruction], 130 | branches: FnvHashSet::default(), 131 | }; 132 | // Add branches if we have any 133 | if let Some(branches) = exit_branches.get(&addr) { 134 | for branch in branches { 135 | current_block.branches.insert(*branch); 136 | } 137 | } 138 | blocks.insert(addr, current_block); 139 | } 140 | blocks 141 | } 142 | 143 | #[derive(Copy, Clone, Debug, PartialEq)] 144 | pub enum BranchTarget { 145 | Dynamic, 146 | Absolute(u32), 147 | Return(u32), 148 | } -------------------------------------------------------------------------------- /relooper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "relooper" 3 | version = "0.1.0" 4 | authors = ["Dannii Willis "] 5 | edition = "2018" 6 | description = "Turn unstructured branches and jumps into structured blocks" 7 | homepage = "https://github.com/curiousdannii/if-decompiler" 8 | license = "MIT" 9 | repository = "https://github.com/curiousdannii/if-decompiler" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | fnv = "1.0.7" 15 | petgraph = "0.6.0" -------------------------------------------------------------------------------- /relooper/README.md: -------------------------------------------------------------------------------- 1 | Relooper 2 | ======== 3 | 4 | Relooper turns unstructured branches and jumps into [structured blocks](https://en.wikipedia.org/wiki/Structured_programming). 5 | 6 | Inspired by the [Cheerp Stackifier algorithm](https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2) and the [Relooper algorithm paper by Alon Zakai](https://github.com/emscripten-core/emscripten/blob/master/docs/paper.pdf). 7 | 8 | More details on the precise algorithm this package implements to come. -------------------------------------------------------------------------------- /relooper/src/tests/glulxercise.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Tests from Glulxercise 4 | ====================== 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use super::*; 13 | 14 | // Float 15 | #[test] 16 | fn float() { 17 | let input64978 = vec![ 18 | (64978, vec![64991, 65011]), 19 | (64991, vec![65001, 65023]), 20 | (65001, vec![65011, 65023]), 21 | (65011, vec![]), 22 | (65023, vec![]), 23 | ]; 24 | let result = reloop(input64978, 64978); 25 | assert_eq!(result, Box::new(Simple(SimpleBlock { 26 | label: 64978, 27 | immediate: Some(Box::new(Multiple(MultipleBlock { 28 | handled: vec![ 29 | basic_handled(64991, Simple(SimpleBlock { 30 | label: 64991, 31 | immediate: Some(Box::new(Multiple(MultipleBlock { 32 | handled: vec![ 33 | basic_handled(65001, Simple(SimpleBlock { 34 | label: 65001, 35 | immediate: None, 36 | branches: FnvHashMap::from_iter(vec![ 37 | (65011, MergedBranchIntoMulti), 38 | (65023, MergedBranchIntoMulti), 39 | ]), 40 | next: None, 41 | })), 42 | ], 43 | }))), 44 | branches: branch_to(65023, MergedBranchIntoMulti), 45 | next: Some(Box::new(Multiple(MultipleBlock { 46 | handled: vec![ 47 | basic_handled(65023, end_node(65023, None)), 48 | ], 49 | }))), 50 | })), 51 | ], 52 | }))), 53 | branches: branch_to(65011, MergedBranchIntoMulti), 54 | next: Some(Box::new(Multiple(MultipleBlock { 55 | handled: vec![ 56 | basic_handled(65011, end_node(65011, None)), 57 | ], 58 | }))), 59 | }))); 60 | } 61 | 62 | // LookSub 63 | #[test] 64 | fn looksub() { 65 | let input3686 = vec![ 66 | (3686, vec![3708]), 67 | (3708, vec![3724, 3798]), 68 | (3724, vec![3728, 3737]), 69 | (3728, vec![3737, 3750]), 70 | (3737, vec![3761]), 71 | (3750, vec![3758, 3761]), 72 | (3758, vec![3798]), 73 | (3761, vec![3765, 3771]), 74 | (3765, vec![3771]), 75 | (3771, vec![3798]), 76 | (3798, vec![3708, 3808]), 77 | (3808, vec![3818, 3833]), 78 | (3818, vec![3833]), 79 | (3833, vec![]), 80 | ]; 81 | 82 | let result = reloop(input3686, 3686); 83 | assert_eq!(result, Box::new(Simple(SimpleBlock { 84 | label: 3686, 85 | immediate: Some(Box::new(Loop(LoopBlock { 86 | loop_id: 0, 87 | inner: Box::new(Simple(SimpleBlock { 88 | label: 3708, 89 | immediate: Some(Box::new(Multiple(MultipleBlock { 90 | handled: vec![ 91 | basic_handled(3724, Simple(SimpleBlock { 92 | label: 3724, 93 | immediate: Some(Box::new(Multiple(MultipleBlock { 94 | handled: vec![ 95 | basic_handled(3728, Simple(SimpleBlock { 96 | label: 3728, 97 | immediate: Some(Box::new(Multiple(MultipleBlock { 98 | handled: vec![ 99 | basic_handled(3750, Simple(SimpleBlock { 100 | label: 3750, 101 | immediate: Some(Box::new(Multiple(MultipleBlock { 102 | handled: vec![ 103 | basic_handled(3758, end_node(3758, Some(branch_to(3798, MergedBranchIntoMulti)))), 104 | ], 105 | }))), 106 | branches: branch_to(3761, MergedBranchIntoMulti), 107 | next: None, 108 | })), 109 | ], 110 | }))), 111 | branches: branch_to(3737, MergedBranchIntoMulti), 112 | next: None, 113 | })), 114 | ], 115 | }))), 116 | branches: branch_to(3737, MergedBranchIntoMulti), 117 | next: Some(Box::new(Multiple(MultipleBlock { 118 | handled: vec![ 119 | basic_handled_without_break(3737, end_node(3737, Some(branch_to(3761, MergedBranch)))), 120 | basic_handled(3761, Simple(SimpleBlock { 121 | label: 3761, 122 | immediate: Some(Box::new(Multiple(MultipleBlock { 123 | handled: vec![ 124 | basic_handled(3765, end_node(3765, Some(branch_to(3771, MergedBranch)))), 125 | ], 126 | }))), 127 | branches: branch_to(3771, MergedBranch), 128 | next: Some(Box::new(end_node(3771, Some(branch_to(3798, MergedBranchIntoMulti))))), 129 | })), 130 | ], 131 | }))), 132 | })), 133 | ], 134 | }))), 135 | branches: branch_to(3798, MergedBranchIntoMulti), 136 | next: Some(Box::new(Multiple(MultipleBlock { 137 | handled: vec![ 138 | basic_handled(3798, Simple(SimpleBlock { 139 | label: 3798, 140 | immediate: Some(Box::new(Multiple(MultipleBlock { 141 | handled: vec![ 142 | basic_handled(3808, Simple(SimpleBlock { 143 | label: 3808, 144 | immediate: Some(Box::new(Multiple(MultipleBlock { 145 | handled: vec![ 146 | basic_handled(3818, end_node(3818, Some(branch_to(3833, MergedBranch)))), 147 | ], 148 | }))), 149 | branches: branch_to(3833, MergedBranch), 150 | next: Some(Box::new(end_node(3833, None))), 151 | })), 152 | ], 153 | }))), 154 | branches: branch_to(3708, LoopContinue(0)), 155 | next: None, 156 | })), 157 | ], 158 | }))), 159 | })), 160 | next: None, 161 | }))), 162 | branches: FnvHashMap::default(), 163 | next: None, 164 | }))); 165 | } 166 | 167 | // Tokenise__ 168 | #[test] 169 | fn tokenise() { 170 | let input727 = vec![ 171 | (727, vec![749]), 172 | (749, vec![756, 959]), 173 | (756, vec![762, 786]), 174 | (762, vec![777, 786]), 175 | (777, vec![756]), 176 | (786, vec![792, 796]), 177 | (792, vec![959]), 178 | (796, vec![819, 831]), 179 | (819, vec![825, 831]), 180 | (825, vec![831, 840]), 181 | (831, vec![892]), 182 | (840, vec![846, 892]), 183 | (846, vec![865, 892]), 184 | (865, vec![871, 892]), 185 | (871, vec![877, 892]), 186 | (877, vec![883, 892]), 187 | (883, vec![840]), 188 | (892, vec![952, 955]), 189 | (952, vec![959]), 190 | (955, vec![749]), 191 | (959, vec![990]), 192 | (990, vec![997, 1254]), 193 | (997, vec![1041, 1045]), 194 | (1041, vec![1045]), 195 | (1045, vec![1054]), 196 | (1054, vec![1060, 1139]), 197 | (1060, vec![1089, 1095]), 198 | (1089, vec![1095, 1122]), 199 | (1095, vec![1130]), 200 | (1122, vec![1130]), 201 | (1130, vec![1054]), 202 | (1139, vec![1145, 1198]), 203 | (1145, vec![1150, 1156]), 204 | (1150, vec![1156, 1181]), 205 | (1156, vec![1189]), 206 | (1181, vec![1189]), 207 | (1189, vec![1139]), 208 | (1198, vec![990]), 209 | (1254, vec![]), 210 | ]; 211 | 212 | let loop749id = 1; 213 | let loop756id = 5; 214 | let loop840id = 4; 215 | let loop990id = 0; 216 | let loop1054id = 3; 217 | let loop1139id = 2; 218 | 219 | let loop1139 = Box::new(Loop(LoopBlock { 220 | loop_id: loop1139id, 221 | inner: Box::new(Simple(SimpleBlock { 222 | label: 1139, 223 | immediate: Some(Box::new(Multiple(MultipleBlock { 224 | handled: vec![ 225 | basic_handled(1145, Simple(SimpleBlock { 226 | label: 1145, 227 | immediate: Some(Box::new(Multiple(MultipleBlock { 228 | handled: vec![ 229 | basic_handled(1150, Simple(SimpleBlock { 230 | label: 1150, 231 | immediate: Some(Box::new(Multiple(MultipleBlock { 232 | handled: vec![ 233 | basic_handled(1181, end_node(1181, Some(branch_to(1189, MergedBranchIntoMulti)))), 234 | ], 235 | }))), 236 | branches: branch_to(1156, MergedBranchIntoMulti), 237 | next: None, 238 | })), 239 | ], 240 | }))), 241 | branches: branch_to(1156, MergedBranchIntoMulti), 242 | next: Some(Box::new(Multiple(MultipleBlock { 243 | handled: vec![ 244 | basic_handled_without_break(1156, end_node(1156, Some(branch_to(1189, MergedBranch)))), 245 | basic_handled(1189, end_node(1189, Some(branch_to(1139, LoopContinue(loop1139id))))), 246 | ], 247 | }))), 248 | })), 249 | basic_handled(1198, end_node(1198, Some(branch_to(990, LoopContinue(loop990id))))), 250 | ], 251 | }))), 252 | branches: FnvHashMap::default(), 253 | next: None 254 | })), 255 | next: None, 256 | })); 257 | 258 | let loop1054 = Box::new(Loop(LoopBlock { 259 | loop_id: loop1054id, 260 | inner: Box::new(Simple(SimpleBlock { 261 | label: 1054, 262 | immediate: Some(Box::new(Multiple(MultipleBlock { 263 | handled: vec![ 264 | basic_handled(1060, Simple(SimpleBlock { 265 | label: 1060, 266 | immediate: Some(Box::new(Multiple(MultipleBlock { 267 | handled: vec![ 268 | basic_handled(1089, Simple(SimpleBlock { 269 | label: 1089, 270 | immediate: Some(Box::new(Multiple(MultipleBlock { 271 | handled: vec![ 272 | basic_handled(1122, end_node(1122, Some(branch_to(1130, MergedBranchIntoMulti)))), 273 | ], 274 | }))), 275 | branches: branch_to(1095, MergedBranchIntoMulti), 276 | next: None, 277 | })), 278 | ], 279 | }))), 280 | branches: branch_to(1095, MergedBranchIntoMulti), 281 | next: Some(Box::new(Multiple(MultipleBlock { 282 | handled: vec![ 283 | basic_handled_without_break(1095, end_node(1095, Some(branch_to(1130, MergedBranch)))), 284 | basic_handled(1130, end_node(1130, Some(branch_to(1054, LoopContinue(loop1054id))))), 285 | ], 286 | }))), 287 | })), 288 | basic_handled(1139, *loop1139), 289 | ], 290 | }))), 291 | branches: FnvHashMap::default(), 292 | next: None, 293 | })), 294 | next: None, 295 | })); 296 | 297 | let loop990 = Box::new(Loop(LoopBlock { 298 | loop_id: loop990id, 299 | inner: Box::new(Simple(SimpleBlock { 300 | label: 990, 301 | immediate: Some(Box::new(Multiple(MultipleBlock { 302 | handled: vec![ 303 | basic_handled(997, Simple(SimpleBlock { 304 | label: 997, 305 | immediate: Some(Box::new(Multiple(MultipleBlock { 306 | handled: vec![ 307 | basic_handled(1041, end_node(1041, Some(branch_to(1045, MergedBranch)))), 308 | ], 309 | }))), 310 | branches: branch_to(1045, MergedBranch), 311 | next: Some(Box::new(Simple(SimpleBlock { 312 | label: 1045, 313 | immediate: Some(loop1054), 314 | branches: FnvHashMap::default(), 315 | next: None, 316 | }))), 317 | })), 318 | basic_handled(1254, end_node(1254, None)), 319 | ], 320 | }))), 321 | branches: FnvHashMap::default(), 322 | next: None, 323 | })), 324 | next: None, 325 | })); 326 | 327 | let loop840branch = branch_to(892, LoopBreakIntoMulti(loop840id)); 328 | let blocks840 = Box::new(Loop(LoopBlock { 329 | loop_id: loop840id, 330 | inner: Box::new(Simple(SimpleBlock { 331 | label: 840, 332 | immediate: Some(Box::new(Multiple(MultipleBlock { 333 | handled: vec![ 334 | basic_handled(846, Simple(SimpleBlock { 335 | label: 846, 336 | immediate: Some(Box::new(Multiple(MultipleBlock { 337 | handled: vec![ 338 | basic_handled(865, Simple(SimpleBlock { 339 | label: 865, 340 | immediate: Some(Box::new(Multiple(MultipleBlock { 341 | handled: vec![ 342 | basic_handled(871, Simple(SimpleBlock { 343 | label: 871, 344 | immediate: Some(Box::new(Multiple(MultipleBlock { 345 | handled: vec![ 346 | basic_handled(877, Simple(SimpleBlock { 347 | label: 877, 348 | immediate: Some(Box::new(Multiple(MultipleBlock { 349 | handled: vec![ 350 | basic_handled(883, end_node(883, Some(branch_to(840, LoopContinue(loop840id))))), 351 | ], 352 | }))), 353 | branches: loop840branch.clone(), 354 | next: None, 355 | })), 356 | ], 357 | }))), 358 | branches: loop840branch.clone(), 359 | next: None, 360 | })), 361 | ], 362 | }))), 363 | branches: loop840branch.clone(), 364 | next: None, 365 | })), 366 | ], 367 | }))), 368 | branches: loop840branch.clone(), 369 | next: None, 370 | })), 371 | ], 372 | }))), 373 | branches: loop840branch.clone(), 374 | next: None, 375 | })), 376 | next: None, 377 | })); 378 | 379 | let blocks786 = Box::new(Simple(SimpleBlock { 380 | label: 786, 381 | immediate: Some(Box::new(Multiple(MultipleBlock { 382 | handled: vec![ 383 | basic_handled(792, end_node(792, Some(branch_to(959, LoopBreakIntoMulti(loop756id))))), 384 | basic_handled(796, Simple(SimpleBlock { 385 | label: 796, 386 | immediate: Some(Box::new(Multiple(MultipleBlock { 387 | handled: vec![ 388 | basic_handled(819, Simple(SimpleBlock { 389 | label: 819, 390 | immediate: Some(Box::new(Multiple(MultipleBlock { 391 | handled: vec![ 392 | basic_handled(825, Simple(SimpleBlock { 393 | label: 825, 394 | immediate: Some(Box::new(Multiple(MultipleBlock { 395 | handled: vec![ 396 | basic_handled(840, *blocks840), 397 | ], 398 | }))), 399 | branches: branch_to(831, MergedBranchIntoMulti), 400 | next: None, 401 | })), 402 | ], 403 | }))), 404 | branches: branch_to(831, MergedBranchIntoMulti), 405 | next: None, 406 | })), 407 | ], 408 | }))), 409 | branches: branch_to(831, MergedBranchIntoMulti), 410 | next: Some(Box::new(Multiple(MultipleBlock { 411 | handled: vec![ 412 | basic_handled_without_break(831, end_node(831, Some(branch_to(892, MergedBranch)))), 413 | basic_handled(892, Simple(SimpleBlock { 414 | label: 892, 415 | immediate: Some(Box::new(Multiple(MultipleBlock { 416 | handled: vec![ 417 | basic_handled(952, end_node(952, Some(branch_to(959, LoopBreakIntoMulti(loop756id))))), 418 | basic_handled(955, end_node(955, Some(branch_to(749, LoopContinue(loop749id))))), 419 | ], 420 | }))), 421 | branches: FnvHashMap::default(), 422 | next: None, 423 | })), 424 | ], 425 | }))), 426 | })), 427 | ], 428 | }))), 429 | branches: FnvHashMap::default(), 430 | next: None, 431 | })); 432 | 433 | let loop756branch = branch_to(786, MergedBranch); 434 | let blocks756 = Box::new(Loop(LoopBlock { 435 | loop_id: loop756id, 436 | inner: Box::new(Simple(SimpleBlock { 437 | label: 756, 438 | immediate: Some(Box::new(Multiple(MultipleBlock { 439 | handled: vec![ 440 | basic_handled(762, Simple(SimpleBlock { 441 | label: 762, 442 | immediate: Some(Box::new(Multiple(MultipleBlock { 443 | handled: vec![ 444 | basic_handled(777, end_node(777, Some(branch_to(756, LoopContinue(loop756id))))), 445 | ], 446 | }))), 447 | branches: loop756branch.clone(), 448 | next: None, 449 | })), 450 | ], 451 | }))), 452 | branches: loop756branch.clone(), 453 | next: Some(blocks786), 454 | })), 455 | next: None, 456 | })); 457 | 458 | let blocks727 = Box::new(Simple(SimpleBlock { 459 | label: 727, 460 | immediate: Some(Box::new(Loop(LoopBlock { 461 | loop_id: loop749id, 462 | inner: Box::new(Simple(SimpleBlock { 463 | label: 749, 464 | immediate: Some(Box::new(Multiple(MultipleBlock { 465 | handled: vec![ 466 | basic_handled(756, *blocks756), 467 | ], 468 | }))), 469 | branches: branch_to(959, MergedBranchIntoMulti), 470 | next: Some(Box::new(Multiple(MultipleBlock { 471 | handled: vec![ 472 | basic_handled(959, Simple(SimpleBlock { 473 | label: 959, 474 | immediate: Some(loop990), 475 | branches: FnvHashMap::default(), 476 | next: None, 477 | })), 478 | ], 479 | }))), 480 | })), 481 | next: None 482 | }))), 483 | branches: FnvHashMap::default(), 484 | next: None, 485 | })); 486 | 487 | let result727 = reloop(input727, 727); 488 | assert_eq!(result727, blocks727); 489 | } -------------------------------------------------------------------------------- /relooper/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Tests for the Relooper 4 | ====================== 5 | 6 | Copyright (c) 2021 Dannii Willis 7 | MIT licenced 8 | https://github.com/curiousdannii/if-decompiler 9 | 10 | */ 11 | 12 | use std::iter::FromIterator; 13 | 14 | use super::*; 15 | use BranchMode::*; 16 | use ShapedBlock::*; 17 | 18 | mod glulxercise; 19 | mod inform6lib; 20 | mod inform7; 21 | 22 | fn basic_handled(label: T, inner: ShapedBlock) -> HandledBlock { 23 | HandledBlock { 24 | labels: vec![label], 25 | inner, 26 | break_after: true, 27 | } 28 | } 29 | 30 | fn basic_handled_without_break(label: T, inner: ShapedBlock) -> HandledBlock { 31 | HandledBlock { 32 | labels: vec![label], 33 | inner, 34 | break_after: false, 35 | } 36 | } 37 | 38 | fn branch_to(label: T, branch: BranchMode) -> FnvHashMap { 39 | let mut res = FnvHashMap::default(); 40 | res.insert(label, branch); 41 | res 42 | } 43 | 44 | fn end_node(label: T, branches: Option>) -> ShapedBlock { 45 | Simple(SimpleBlock { 46 | label, 47 | immediate: None, 48 | branches: branches.unwrap_or_default(), 49 | next: None, 50 | }) 51 | } 52 | 53 | // Basic sequential blocks 54 | #[test] 55 | fn test_basic_blocks() { 56 | let blocks = vec![ 57 | (0, vec![1]), 58 | (1, vec![2]), 59 | (2, vec![]), 60 | ]; 61 | let result = reloop(blocks, 0); 62 | assert_eq!(result, Box::new(Simple(SimpleBlock { 63 | label: 0, 64 | immediate: Some(Box::new(Simple(SimpleBlock { 65 | label: 1, 66 | immediate: Some(Box::new(end_node(2, None))), 67 | branches: FnvHashMap::default(), 68 | next: None, 69 | }))), 70 | branches: FnvHashMap::default(), 71 | next: None, 72 | }))); 73 | } 74 | 75 | // Some basic loops 76 | #[test] 77 | fn test_basic_loops() { 78 | let blocks = vec![ 79 | (0, vec![1]), 80 | (1, vec![2]), 81 | (2, vec![3]), 82 | (3, vec![1]), 83 | ]; 84 | let result = reloop(blocks, 0); 85 | assert_eq!(result, Box::new(Simple(SimpleBlock { 86 | label: 0, 87 | immediate: Some(Box::new(Loop(LoopBlock { 88 | loop_id: 0, 89 | inner: Box::new(Simple(SimpleBlock { 90 | label: 1, 91 | immediate: Some(Box::new(Simple(SimpleBlock { 92 | label: 2, 93 | immediate: Some(Box::new(end_node(3, Some(branch_to(1, LoopContinue(0)))))), 94 | branches: FnvHashMap::default(), 95 | next: None, 96 | }))), 97 | branches: FnvHashMap::default(), 98 | next: None, 99 | })), 100 | next: None, 101 | }))), 102 | branches: FnvHashMap::default(), 103 | next: None, 104 | }))); 105 | 106 | let blocks = vec![ 107 | (0, vec![1]), 108 | (1, vec![2, 4]), 109 | (2, vec![3]), 110 | (3, vec![1]), 111 | (4, vec![]), 112 | ]; 113 | let result = reloop(blocks, 0); 114 | assert_eq!(result, Box::new(Simple(SimpleBlock { 115 | label: 0, 116 | immediate: Some(Box::new(Loop(LoopBlock { 117 | loop_id: 0, 118 | inner: Box::new(Simple(SimpleBlock { 119 | label: 1, 120 | immediate: Some(Box::new(Multiple(MultipleBlock { 121 | handled: vec![ 122 | basic_handled(2, Simple(SimpleBlock { 123 | label: 2, 124 | immediate: Some(Box::new(end_node(3, Some(branch_to(1, LoopContinue(0)))))), 125 | branches: FnvHashMap::default(), 126 | next: None, 127 | })), 128 | basic_handled(4, end_node(4, None)), 129 | ], 130 | }))), 131 | branches: FnvHashMap::default(), 132 | next: None, 133 | })), 134 | next: None, 135 | }))), 136 | branches: FnvHashMap::default(), 137 | next: None, 138 | }))); 139 | 140 | let blocks = vec![ 141 | (0, vec![1]), 142 | (1, vec![2]), 143 | (2, vec![3, 4]), 144 | (3, vec![1]), 145 | (4, vec![]), 146 | ]; 147 | let result = reloop(blocks, 0); 148 | assert_eq!(result, Box::new(Simple(SimpleBlock { 149 | label: 0, 150 | immediate: Some(Box::new(Loop(LoopBlock { 151 | loop_id: 0, 152 | inner: Box::new(Simple(SimpleBlock { 153 | label: 1, 154 | immediate: Some(Box::new(Simple(SimpleBlock { 155 | label: 2, 156 | immediate: Some(Box::new(Multiple(MultipleBlock { 157 | handled: vec![ 158 | basic_handled(3, end_node(3, Some(branch_to(1, LoopContinue(0))))), 159 | basic_handled(4, end_node(4, None)), 160 | ], 161 | }))), 162 | branches: FnvHashMap::default(), 163 | next: None, 164 | }))), 165 | branches: FnvHashMap::default(), 166 | next: None, 167 | })), 168 | next: None, 169 | }))), 170 | branches: FnvHashMap::default(), 171 | next: None, 172 | }))); 173 | 174 | // Test a self loop 175 | let blocks = vec![ 176 | (0, vec![0, 1]), 177 | (1, vec![]), 178 | ]; 179 | let result = reloop(blocks, 0); 180 | assert_eq!(result, Box::new(Loop(LoopBlock { 181 | loop_id: 0, 182 | inner: Box::new(Simple(SimpleBlock { 183 | label: 0, 184 | immediate: Some(Box::new(Multiple(MultipleBlock { 185 | handled: vec![ 186 | basic_handled(1, end_node(1, None)), 187 | ], 188 | }))), 189 | branches: branch_to(0, LoopContinue(0)), 190 | next: None, 191 | })), 192 | next: None, 193 | }))); 194 | 195 | // Multiple breaks to a dominated node from a loop (a little excerpt from the Glulxercise Tokenise test) 196 | let blocks = vec![ 197 | (749, vec![756]), 198 | (756, vec![762, 786]), 199 | (762, vec![777, 786]), 200 | (777, vec![756]), 201 | (786, vec![]), 202 | ]; 203 | let result = reloop(blocks, 749); 204 | assert_eq!(result, Box::new(Simple(SimpleBlock { 205 | label: 749, 206 | immediate: Some(Box::new(Loop(LoopBlock { 207 | loop_id: 0, 208 | inner: Box::new(Simple(SimpleBlock { 209 | label: 756, 210 | immediate: Some(Box::new(Multiple(MultipleBlock { 211 | handled: vec![ 212 | basic_handled(762, Simple(SimpleBlock { 213 | label: 762, 214 | immediate: Some(Box::new(Multiple(MultipleBlock { 215 | handled: vec![ 216 | basic_handled(777, end_node(777, Some(branch_to(756, LoopContinue(0))))), 217 | ], 218 | }))), 219 | branches: branch_to(786, MergedBranch), 220 | next: None, 221 | })), 222 | ], 223 | }))), 224 | branches: branch_to(786, MergedBranch), 225 | next: Some(Box::new(end_node(786, None))), 226 | })), 227 | next: None, 228 | }))), 229 | branches: FnvHashMap::default(), 230 | next: None, 231 | }))); 232 | } 233 | 234 | // Some basic ifs 235 | #[test] 236 | fn test_basic_ifs() { 237 | let blocks = vec![ 238 | (0, vec![1, 2]), 239 | (1, vec![]), 240 | (2, vec![]), 241 | ]; 242 | let result = reloop(blocks, 0); 243 | assert_eq!(result, Box::new(Simple(SimpleBlock { 244 | label: 0, 245 | immediate: Some(Box::new(Multiple(MultipleBlock { 246 | handled: vec![ 247 | basic_handled(1, end_node(1, None)), 248 | basic_handled(2, end_node(2, None)), 249 | ], 250 | }))), 251 | branches: FnvHashMap::default(), 252 | next: None, 253 | }))); 254 | 255 | let blocks = vec![ 256 | (0, vec![1, 2]), 257 | (1, vec![3]), 258 | (2, vec![3]), 259 | (3, vec![]), 260 | ]; 261 | let result = reloop(blocks, 0); 262 | assert_eq!(result, Box::new(Simple(SimpleBlock { 263 | label: 0, 264 | immediate: Some(Box::new(Multiple(MultipleBlock { 265 | handled: vec![ 266 | basic_handled(1, end_node(1, Some(branch_to(3, MergedBranch)))), 267 | basic_handled(2, end_node(2, Some(branch_to(3, MergedBranch)))), 268 | ], 269 | }))), 270 | branches: FnvHashMap::default(), 271 | next: Some(Box::new(end_node(3, None))), 272 | }))); 273 | 274 | let blocks = vec![ 275 | (0, vec![1, 2]), 276 | (1, vec![2]), 277 | (2, vec![]), 278 | ]; 279 | let result = reloop(blocks, 0); 280 | assert_eq!(result, Box::new(Simple(SimpleBlock { 281 | label: 0, 282 | immediate: Some(Box::new(Multiple(MultipleBlock { 283 | handled: vec![ 284 | basic_handled(1, end_node(1, Some(branch_to(2, MergedBranch)))), 285 | ], 286 | }))), 287 | branches: branch_to(2, MergedBranch), 288 | next: Some(Box::new(end_node(2, None))), 289 | }))); 290 | } 291 | 292 | #[test] 293 | fn test_nested_loops() { 294 | let blocks = vec![ 295 | (0, vec![1, 3]), 296 | (1, vec![2]), 297 | (2, vec![0, 1]), 298 | (3, vec![]), 299 | ]; 300 | let result = reloop(blocks, 0); 301 | assert_eq!(result, Box::new(Loop(LoopBlock { 302 | loop_id: 0, 303 | inner: Box::new(Simple(SimpleBlock { 304 | label: 0, 305 | immediate: Some(Box::new(Multiple(MultipleBlock { 306 | handled: vec![ 307 | basic_handled(1, Loop(LoopBlock { 308 | loop_id: 1, 309 | inner: Box::new(Simple(SimpleBlock { 310 | label: 1, 311 | immediate: Some(Box::new(Simple(SimpleBlock { 312 | label: 2, 313 | immediate: None, 314 | branches: FnvHashMap::from_iter(vec![ 315 | (0, LoopContinue(0)), 316 | (1, LoopContinue(1)), 317 | ]), 318 | next: None, 319 | }))), 320 | branches: FnvHashMap::default(), 321 | next: None, 322 | })), 323 | next: None, 324 | })), 325 | basic_handled(3, end_node(3, None)), 326 | ], 327 | }))), 328 | branches: FnvHashMap::default(), 329 | next: None, 330 | })), 331 | next: None, 332 | }))); 333 | } 334 | 335 | mod nested_branches { 336 | use super::*; 337 | 338 | #[test] 339 | fn simple_nested_branches() { 340 | let blocks = vec![ 341 | (0, vec![1, 5]), 342 | (1, vec![2, 3]), 343 | (2, vec![4]), 344 | (3, vec![4]), 345 | (4, vec![8]), 346 | (5, vec![6, 7]), 347 | (6, vec![8]), 348 | (7, vec![8]), 349 | (8, vec![]), 350 | ]; 351 | let result = reloop(blocks, 0); 352 | assert_eq!(result, Box::new(Simple(SimpleBlock { 353 | label: 0, 354 | immediate: Some(Box::new(Multiple(MultipleBlock { 355 | handled: vec![ 356 | basic_handled(1, Simple(SimpleBlock { 357 | label: 1, 358 | immediate: Some(Box::new(Multiple(MultipleBlock { 359 | handled: vec![ 360 | basic_handled(2, end_node(2, Some(branch_to(4, MergedBranch)))), 361 | basic_handled(3, end_node(3, Some(branch_to(4, MergedBranch)))), 362 | ], 363 | }))), 364 | next: Some(Box::new(end_node(4, Some(branch_to(8, MergedBranch))))), 365 | branches: FnvHashMap::default(), 366 | })), 367 | basic_handled(5, Simple(SimpleBlock { 368 | label: 5, 369 | immediate: Some(Box::new(Multiple(MultipleBlock { 370 | handled: vec![ 371 | basic_handled(6, end_node(6, Some(branch_to(8, MergedBranch)))), 372 | basic_handled(7, end_node(7, Some(branch_to(8, MergedBranch)))), 373 | ], 374 | }))), 375 | branches: FnvHashMap::default(), 376 | next: None, 377 | })), 378 | ], 379 | }))), 380 | branches: FnvHashMap::default(), 381 | next: Some(Box::new(end_node(8, None))), 382 | }))); 383 | } 384 | 385 | // A small part of Glulxercise Tokenise 386 | // This represents and if-else block where the if clause has two conditions with an OR 387 | // (In this case it is a strict mode range check - if writing outside an array's bounds show an error, if within perform the write) 388 | #[test] 389 | fn if_else_with_or() { 390 | let blocks = vec![ 391 | (1060, vec![1089, 1095]), 392 | (1089, vec![1095, 1122]), 393 | (1095, vec![1130]), 394 | (1122, vec![1130]), 395 | (1130, vec![]), 396 | ]; 397 | let result = reloop(blocks, 1060); 398 | assert_eq!(result, Box::new(Simple(SimpleBlock { 399 | label: 1060, 400 | immediate: Some(Box::new(Multiple(MultipleBlock { 401 | handled: vec![ 402 | basic_handled(1089, Simple(SimpleBlock { 403 | label: 1089, 404 | immediate: Some(Box::new(Multiple(MultipleBlock { 405 | handled: vec![ 406 | basic_handled(1122, end_node(1122, Some(branch_to(1130, MergedBranchIntoMulti)))), 407 | ], 408 | }))), 409 | branches: branch_to(1095, MergedBranchIntoMulti), 410 | next: None, 411 | })), 412 | ], 413 | }))), 414 | branches: branch_to(1095, MergedBranchIntoMulti), 415 | next: Some(Box::new(Multiple(MultipleBlock { 416 | handled: vec![ 417 | basic_handled_without_break(1095, end_node(1095, Some(branch_to(1130, MergedBranch)))), 418 | basic_handled(1130, end_node(1130, None)), 419 | ], 420 | }))), 421 | }))); 422 | } 423 | } 424 | 425 | #[test] 426 | fn test_loop_in_branch() { 427 | let blocks = vec![ 428 | (0, vec![1, 2]), 429 | (1, vec![4]), 430 | (2, vec![3]), 431 | (3, vec![2, 4]), 432 | (4, vec![]), 433 | ]; 434 | let result = reloop(blocks, 0); 435 | assert_eq!(result, Box::new(Simple(SimpleBlock { 436 | label: 0, 437 | immediate: Some(Box::new(Multiple(MultipleBlock { 438 | handled: vec![ 439 | basic_handled(1, end_node(1, Some(branch_to(4, MergedBranch)))), 440 | basic_handled(2, Loop(LoopBlock { 441 | loop_id: 0, 442 | inner: Box::new(Simple(SimpleBlock { 443 | label: 2, 444 | immediate: Some(Box::new(Simple(SimpleBlock { 445 | label: 3, 446 | immediate: None, 447 | branches: FnvHashMap::from_iter(vec![ 448 | (2, LoopContinue(0)), 449 | (4, LoopBreak(0)), 450 | ]), 451 | next: None, 452 | }))), 453 | branches: FnvHashMap::default(), 454 | next: None, 455 | })), 456 | next: None, 457 | })), 458 | ], 459 | }))), 460 | branches: FnvHashMap::default(), 461 | next: Some(Box::new(end_node(4, None))), 462 | }))); 463 | } 464 | 465 | #[test] 466 | fn test_spaghetti() { 467 | let blocks = vec![ 468 | (0, vec![1, 2]), 469 | (1, vec![3, 4]), 470 | (2, vec![5, 6]), 471 | (3, vec![8]), 472 | (4, vec![7]), 473 | (5, vec![7]), 474 | (6, vec![8]), 475 | (7, vec![]), 476 | (8, vec![]), 477 | ]; 478 | let result = reloop(blocks, 0); 479 | assert_eq!(result, Box::new(Simple(SimpleBlock { 480 | label: 0, 481 | immediate: Some(Box::new(Multiple(MultipleBlock { 482 | handled: vec![ 483 | basic_handled(1, Simple(SimpleBlock { 484 | label: 1, 485 | immediate: Some(Box::new(Multiple(MultipleBlock { 486 | handled: vec![ 487 | basic_handled(3, end_node(3, Some(branch_to(8, MergedBranchIntoMulti)))), 488 | basic_handled(4, end_node(4, Some(branch_to(7, MergedBranchIntoMulti)))), 489 | ], 490 | }))), 491 | branches: FnvHashMap::default(), 492 | next: None, 493 | })), 494 | basic_handled(2, Simple(SimpleBlock { 495 | label: 2, 496 | immediate: Some(Box::new(Multiple(MultipleBlock { 497 | handled: vec![ 498 | basic_handled(5, end_node(5, Some(branch_to(7, MergedBranchIntoMulti)))), 499 | basic_handled(6, end_node(6, Some(branch_to(8, MergedBranchIntoMulti)))), 500 | ], 501 | }))), 502 | branches: FnvHashMap::default(), 503 | next: None, 504 | })), 505 | ], 506 | }))), 507 | branches: FnvHashMap::default(), 508 | next: Some(Box::new(Multiple(MultipleBlock { 509 | handled: vec![ 510 | basic_handled(7, end_node(7, None)), 511 | basic_handled(8, end_node(8, None)), 512 | ], 513 | }))), 514 | }))); 515 | 516 | let blocks = vec![ 517 | (0, vec![1, 2, 3]), 518 | (1, vec![6]), 519 | (2, vec![4, 5]), 520 | (3, vec![7]), 521 | (4, vec![2, 6]), 522 | (5, vec![2, 7]), 523 | (6, vec![]), 524 | (7, vec![]), 525 | ]; 526 | let result = reloop(blocks, 0); 527 | assert_eq!(result, Box::new(Simple(SimpleBlock { 528 | label: 0, 529 | immediate: Some(Box::new(Multiple(MultipleBlock { 530 | handled: vec![ 531 | basic_handled(1, end_node(1, Some(branch_to(6, MergedBranchIntoMulti)))), 532 | basic_handled(2, Loop(LoopBlock { 533 | loop_id: 0, 534 | inner: Box::new(Simple(SimpleBlock { 535 | label: 2, 536 | immediate: Some(Box::new(Multiple(MultipleBlock { 537 | handled: vec![ 538 | basic_handled(4, Simple(SimpleBlock { 539 | label: 4, 540 | immediate: None, 541 | branches: FnvHashMap::from_iter(vec![ 542 | (2, LoopContinue(0)), 543 | (6, LoopBreakIntoMulti(0)), 544 | ]), 545 | next: None, 546 | })), 547 | basic_handled(5, Simple(SimpleBlock { 548 | label: 5, 549 | immediate: None, 550 | branches: FnvHashMap::from_iter(vec![ 551 | (2, LoopContinue(0)), 552 | (7, LoopBreakIntoMulti(0)), 553 | ]), 554 | next: None, 555 | })), 556 | ], 557 | }))), 558 | branches: FnvHashMap::default(), 559 | next: None, 560 | })), 561 | next: None, 562 | })), 563 | basic_handled(3, end_node(3, Some(branch_to(7, MergedBranchIntoMulti)))), 564 | ], 565 | }))), 566 | branches: FnvHashMap::default(), 567 | next: Some(Box::new(Multiple(MultipleBlock { 568 | handled: vec![ 569 | basic_handled(6, end_node(6, None)), 570 | basic_handled(7, end_node(7, None)), 571 | ], 572 | }))), 573 | }))); 574 | } 575 | 576 | // The example from the Stackifier article 577 | #[test] 578 | fn test_stackifier_multiloop() { 579 | let blocks = vec![ 580 | ('A', vec!['B', 'C']), 581 | ('B', vec!['D', 'E']), 582 | ('C', vec!['E']), 583 | ('D', vec!['B', 'C']), 584 | ('E', vec!['F', 'G']), 585 | ('F', vec!['G']), 586 | ('G', vec!['B', 'H']), 587 | ('H', vec![]), 588 | ]; 589 | let result = reloop(blocks, 'A'); 590 | assert_eq!(result, Box::new(Simple(SimpleBlock { 591 | label: 'A', 592 | immediate: Some(Box::new(Loop(LoopBlock { 593 | loop_id: 0, 594 | inner: Box::new(Multiple(MultipleBlock { 595 | handled: vec![ 596 | basic_handled('B', Simple(SimpleBlock { 597 | label: 'B', 598 | immediate: Some(Box::new(Multiple(MultipleBlock { 599 | handled: vec![ 600 | basic_handled('D', Simple(SimpleBlock { 601 | label: 'D', 602 | immediate: None, 603 | branches: FnvHashMap::from_iter(vec![ 604 | ('B', LoopContinueIntoMulti(0)), 605 | ('C', LoopContinueIntoMulti(0)), 606 | ]), 607 | next: None, 608 | })), 609 | ], 610 | }))), 611 | branches: branch_to('E', MergedBranch), 612 | next: None, 613 | })), 614 | basic_handled('C', end_node('C', Some(branch_to('E', MergedBranch)))), 615 | ], 616 | })), 617 | next: Some(Box::new(Simple(SimpleBlock { 618 | label: 'E', 619 | immediate: Some(Box::new(Multiple(MultipleBlock { 620 | handled: vec![ 621 | basic_handled('F', end_node('F', Some(branch_to('G', MergedBranch)))), 622 | ], 623 | }))), 624 | branches: branch_to('G', MergedBranch), 625 | next: Some(Box::new(Simple(SimpleBlock { 626 | label: 'G', 627 | immediate: Some(Box::new(Multiple(MultipleBlock { 628 | handled: vec![ 629 | basic_handled('H', end_node('H', None)), 630 | ], 631 | }))), 632 | branches: branch_to('B', LoopContinueIntoMulti(0)), 633 | next: None, 634 | }))), 635 | }))), 636 | }))), 637 | branches: FnvHashMap::default(), 638 | next: None, 639 | }))); 640 | } 641 | 642 | #[test] 643 | fn test_loopmulti() { 644 | // Test a LoopMulti with a top triple branch 645 | let blocks = vec![ 646 | (1, vec![2, 3, 4]), 647 | (2, vec![]), 648 | (3, vec![4]), 649 | (4, vec![5]), 650 | (5, vec![3]), 651 | ]; 652 | let result = reloop(blocks, 1); 653 | assert_eq!(result, Box::new(Simple(SimpleBlock { 654 | label: 1, 655 | immediate: Some(Box::new(Multiple(MultipleBlock { 656 | handled: vec![ 657 | basic_handled(2, end_node(2, None)), 658 | HandledBlock { 659 | labels: vec![3, 4], 660 | inner: Loop(LoopBlock { 661 | loop_id: 0, 662 | inner: Box::new(Multiple(MultipleBlock { 663 | handled: vec![ 664 | basic_handled(3, end_node(3, Some(branch_to(4, LoopContinueIntoMulti(0))))), 665 | basic_handled(4, Simple(SimpleBlock { 666 | label: 4, 667 | immediate: Some(Box::new(end_node(5, Some(branch_to(3, LoopContinueIntoMulti(0)))))), 668 | branches: FnvHashMap::default(), 669 | next: None, 670 | })), 671 | ], 672 | })), 673 | next: None, 674 | }), 675 | break_after: true, 676 | }, 677 | ], 678 | }))), 679 | branches: FnvHashMap::default(), 680 | next: None, 681 | }))); 682 | 683 | // Test a multiloop with multiple parents, internal branches, and a rejoined exit branch 684 | let blocks = vec![ 685 | (0, vec![1, 6]), 686 | (1, vec![2, 3]), 687 | (2, vec![3, 4]), 688 | (3, vec![4, 5]), 689 | (4, vec![5]), 690 | (5, vec![3, 6]), 691 | (6, vec![]), 692 | ]; 693 | let result = reloop(blocks, 0); 694 | assert_eq!(result, Box::new(Simple(SimpleBlock { 695 | label: 0, 696 | immediate: Some(Box::new(Multiple(MultipleBlock { 697 | handled: vec![ 698 | basic_handled(1, Simple(SimpleBlock { 699 | label: 1, 700 | immediate: Some(Box::new(Multiple(MultipleBlock { 701 | handled: vec![ 702 | basic_handled(2, Simple(SimpleBlock { 703 | label: 2, 704 | immediate: None, 705 | branches: FnvHashMap::from_iter(vec![ 706 | (3, MergedBranchIntoMulti), 707 | (4, MergedBranchIntoMulti), 708 | ]), 709 | next: None, 710 | })), 711 | ], 712 | }))), 713 | branches: branch_to(3, MergedBranchIntoMulti), 714 | next: Some(Box::new(Loop(LoopBlock { 715 | loop_id: 0, 716 | inner: Box::new(Multiple(MultipleBlock { 717 | handled: vec![ 718 | basic_handled(3, Simple(SimpleBlock { 719 | label: 3, 720 | immediate: None, 721 | branches: FnvHashMap::from_iter(vec![ 722 | (4, LoopContinueIntoMulti(0)), 723 | (5, MergedBranch), 724 | ]), 725 | next: None, 726 | })), 727 | basic_handled(4, end_node(4, Some(branch_to(5, MergedBranch)))), 728 | ], 729 | })), 730 | next: Some(Box::new(Simple(SimpleBlock { 731 | label: 5, 732 | immediate: None, 733 | branches: FnvHashMap::from_iter(vec![ 734 | (3, LoopContinueIntoMulti(0)), 735 | (6, LoopBreak(0)), 736 | ]), 737 | next: None, 738 | }))), 739 | }))), 740 | })), 741 | ], 742 | }))), 743 | branches: branch_to(6, MergedBranch), 744 | next: Some(Box::new(end_node(6, None))), 745 | }))); 746 | } -------------------------------------------------------------------------------- /tests/advent.ulx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curiousdannii/if-decompiler/e9b0c6886bbb6aff1a4c441fb03f7cb387cbb494/tests/advent.ulx -------------------------------------------------------------------------------- /tests/advent.ulx.regtest: -------------------------------------------------------------------------------- 1 | ** game: advent.ulx 2 | 3 | * prologue 4 | 5 | Welcome to Adventure! 6 | 7 | > east 8 | You are inside a building, a well house for a large spring. 9 | 10 | > get all 11 | set of keys: Taken. 12 | 13 | > west 14 | > south 15 | > south 16 | > south 17 | > unlock grate with key 18 | > open it 19 | > down 20 | > west 21 | > west 22 | > turn on lamp 23 | A three foot black rod with a rusty star on one end lies nearby. -------------------------------------------------------------------------------- /tests/glulxercise.ulx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curiousdannii/if-decompiler/e9b0c6886bbb6aff1a4c441fb03f7cb387cbb494/tests/glulxercise.ulx -------------------------------------------------------------------------------- /tests/glulxercise.ulx.gameinfo.dbg: -------------------------------------------------------------------------------- 1 | glulxercise.infInform 6Main__ 60
60
12
Main 72
72
179
GGInitialise 251
251
80
GGRecoverObjects 331
331
228
Keyboard 559
559
44
KeyboardPrimitive 603
603
119
Tokenise__ 722
722
535
Banner 1257
1257
393
TestLoop 1650
1650
216
PrintShortName 1866
1866
104
Hex 1970
1970
113
string_to_array 2083
2083
120
func_to_array 2203
2203
107
string_to_uniarray 2310
2310
121
check 2431
2431
43
check_nonzero 2474
2474
38
check_range 2512
2512
58
check_hex 2570
2570
61
check_list 2631
2631
105
check_bytelist 2736
2736
93
check_str 2829
2829
210
check_ustr 3039
3039
210
count_failures 3249
3249
58
run_all_tests 3307
3307
271
OpQuitAction.testfunc 3578
3578
17
GlkQuitAction.testfunc 3595
3595
25
AllAction.testfunc 3620
3620
23
AllFloatAction.testfunc 3643
3643
23
TestNothing 3666
3666
15
LookSub 3681
3681
155
OperandTest.testfunc 3836
3836
952
ArithTest.testfunc 4788
4788
2027
BigMulTest.testfunc 6815
6815
1437
CompoundVarTest.testfunc 8252
8252
436
CompoundArithTest.testfunc 8688
8688
594
BitwiseTest.testfunc 9282
9282
618
ShiftTest.testfunc 9900
9900
2665
TruncCopyTest.testfunc 12565
12565
677
ExtendTest.testfunc 13242
13242
909
AloadTest.testfunc 14151
14151
3819
AstoreTest.testfunc 17970
17970
6210
ArrayBitTest.testfunc 24180
24180
1410
CallTest.testfunc 25590
25590
244
tailcalltest 25834
25834
38
tailcall2test 25872
25872
18
CallStackTest.testfunc 25890
25890
1141
arg2adder 27031
27031
12
arghasher 27043
27043
49
noop 27092
27092
6
JumpTest.testfunc 27098
27098
387
test_jumpabs 27485
27485
22
test_jumpabs_2 27507
27507
21
JumpFormTest.testfunc 27528
27528
1268
test_jump0 28796
28796
8
test_jump1 28804
28804
9
test_var_jump 28813
28813
11
test_var_jump0 28824
28824
14
test_var_jump1 28838
28838
15
test_computed_jump 28853
28853
26
test_push_computed_jump 28879
28879
28
test_jz0 28907
28907
11
test_jz1 28918
28918
12
test_push_computed_jz 28930
28930
29
test_jeq0 28959
28959
13
test_jeq1 28972
28972
14
test_push_computed_jeq 28986
28986
31
CompareTest.testfunc 29017
29017
1350
test_branch_jgt 30367
30367
100
test_branch_jge 30467
30467
100
test_branch_jlt 30567
30567
100
test_branch_jle 30667
30667
100
test_branch_jgtu 30767
30767
100
test_branch_jgeu 30867
30867
100
test_branch_jltu 30967
30967
100
test_branch_jleu 31067
31067
100
StackTest.testfunc 31167
31167
3852
GestaltTest.testfunc 35019
35019
226
ThrowTest.testfunc 35245
35245
958
test_catch_jump0 36203
36203
13
test_catch_jump1 36216
36216
21
test_catch_jump1sp 36237
36237
23
test_catch_discard 36260
36260
17
test_catch_computed 36277
36277
28
recurse_throw 36305
36305
36
recurse_throw_sp 36341
36341
40
recurse_throw_spnoop 36381
36381
47
StreamNumTest.testfunc 36428
36428
486
patch_encoding_table 36914
36914
674
StringsTest.testfunc 37588
37588
1293
tablestring 38881
38881
46
tableustring 38927
38927
46
argprint 38973
38973
38
argprintstr 39011
39011
38
nativeprint 39049
39049
59
numberprint 39108
39108
9
counterprint 39117
39117
18
byehello_func 39135
39135
21
RamStringsTest.testfunc 39156
39156
411
IOSysTest.testfunc 39567
39567
911
IOSys2Test.testfunc 40478
40478
76
iosys_tester 40554
40554
66
printhash_func 40620
40620
12
print123_func 40632
40632
9
FilterTest.testfunc 40641
40641
587
tablefilterstring 41228
41228
58
tablefilterrstring 41286
41286
58
bracketfilter 41344
41344
14
prependequal 41358
41358
30
prependuequal 41388
41388
30
appendequal 41418
41418
30
surroundbracket 41448
41448
41
surroundonce 41489
41489
21
NullIOTest.testfunc 41510
41510
587
tablenullstring 42097
42097
53
tablenullrstring 42150
42150
53
GlkTest.testfunc 42203
42203
1229
GiDispaTest.testfunc 43432
43432
81
gidispa_testfunc 43513
43513
141
RandomTest.testfunc 43654
43654
1681
DRandomTest.testfunc 45335
45335
417
SearchTest.testfunc 45752
45752
10028
fill_array_seq 55780
55780
38
fill_array_val 55818
55818
32
MemZeroTest.testfunc 55850
55850
484
MemCopyTest.testfunc 56334
56334
855
UndoTest.testfunc 57189
57189
1331
undo_depth_check 58520
58520
17
undo_depth_check2 58537
58537
24
MultiUndoTest.testfunc 58561
58561
477
RestoreTest.testfunc 59038
59038
1070
save_nested 60108
60108
89
restore_nested 60197
60197
19
delete_if_exists 60216
60216
39
VerifyTest.testfunc 60255
60255
97
ProtectTest.testfunc 60352
60352
821
MemSizeTest.testfunc 61173
61173
734
UndoMemSizeTest.testfunc 61907
61907
1028
UndoRestartTest.testfunc 62935
62935
320
HeapTest.testfunc 63255
63255
917
UndoHeapTest.testfunc 64172
64172
633
do_failure 64805
64805
12
AccelTest.testfunc 64817
64817
156
Float 64973
64973
62
FloatExp 65035
65035
287
FloatDec 65322
65322
318
check_float 65640
65640
91
check_float_e 65731
65731
102
check_isnan 65833
65833
96
FloatConvTest.testfunc 65929
65929
12385
FloatArithTest.testfunc 78314
78314
4480
FloatModTest.testfunc 82794
82794
3322
FloatRoundTest.testfunc 86116
86116
2359
FloatExpTest.testfunc 88475
88475
4715
FloatTrigTest.testfunc 93190
93190
2295
FloatAtan2Test.testfunc 95485
95485
1399
FloatJumpFormTest.testfunc 96884
96884
1228
test_jfeq0 98112
98112
15
test_jfeq1 98127
98127
16
test_push_computed_jfeq 98143
98143
33
FloatJumpTest.testfunc 98176
98176
5541
FloatCompareTest.testfunc 103717
103717
4592
test_jisinf 108309
108309
15
test_jisnan 108324
108324
15
test_jflt 108339
108339
17
test_jfle 108356
108356
17
test_jfgt 108373
108373
17
test_jfge 108390
108390
17
test_jfeq 108407
108407
18
test_jfne 108425
108425
18
FloatprintTest.testfunc 108443
108443
3189
Safari5Test.testfunc 111632
111632
160
Print__PName 111792
111792
167
WV__Pr 111959
111959
44
RV__Pr 112003
112003
65
CA__Pr 112068
112068
552
RA__Pr 112620
112620
132
RL__Pr 112752
112752
138
OP__Pr 112890
112890
99
OC__Cl 112989
112989
275
Copy__Primitive 113264
113264
168
RT__Err 113432
113432
1365
Z__Region 114797
114797
71
Unsigned__Compare 114868
114868
25
Meta__class 114893
114893
110
CP__Tab 115003
115003
69
Cl__Ms 115072
115072
485
RT__ChPS 115557
115557
105
RT__TrPS 115662
115662
53
RT__ChLDB 115715
115715
49
RT__ChLDW 115764
115764
53
RT__ChSTB 115817
115817
74
RT__ChSTW 115891
115891
78
RT__ChPrintC 115969
115969
72
RT__ChPrintA 116041
116041
83
RT__ChPrintS 116124
116124
36
RT__ChPrintO 116160
116160
40
OB__Move 116200
116200
96
OB__Remove 116296
116296
83
Print__Addr 116379
116379
76
Glk__Wrap 116455
116455
27
Symb__Tab 116482
116482
10
R2x1bAADAQIAAnEAAAKsAAACrQAAABAAAAAAPAABxwypuywPSW5mbwABAAA2LjM1MC4zOAAJMTYxMTE0wQAAMA==
2 | -------------------------------------------------------------------------------- /tests/glulxercise.ulx.regtest: -------------------------------------------------------------------------------- 1 | ** game: glulxercise.ulx 2 | 3 | * all 4 | 5 | Glulxercise: A Glulx interpreter unit test 6 | Welcome to the test chamber. 7 | 8 | > all 9 | !FAIL 10 | All tests passed. -------------------------------------------------------------------------------- /tests/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | 7 | cheapglk() { 8 | echo "Downloading Cheapglk" 9 | rm -rf cheapglk 10 | curl -Ls https://github.com/erkyrath/cheapglk/archive/refs/heads/master.tar.gz | tar xz 11 | mv cheapglk-master cheapglk -f 12 | echo "Compiling Cheapglk" 13 | cd cheapglk && make && cd .. 14 | } 15 | 16 | regtest() { 17 | echo "Downloading regtest.py" 18 | curl -s https://raw.githubusercontent.com/erkyrath/plotex/master/regtest.py -o regtest.py 19 | } 20 | 21 | remglk() { 22 | echo "Downloading Remglk" 23 | rm -rf remglk 24 | curl -Ls https://github.com/erkyrath/remglk/archive/refs/heads/master.tar.gz | tar xz 25 | mv remglk-master remglk -f 26 | echo "Compiling Remglk" 27 | cd remglk && make && cd .. 28 | } 29 | 30 | for task in "$@" 31 | do 32 | case "$task" in 33 | cheapglk) cheapglk ;; 34 | regtest) regtest ;; 35 | remglk) remglk ;; 36 | esac 37 | done -------------------------------------------------------------------------------- /tests/runalltests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | 7 | ./runtest.sh -f glulxercise.ulx -d 8 | ./runtest.sh -f glulxercise.ulx -u 27057 9 | ./runtest.sh -f advent.ulx 10 | ./runtest.sh -f advent.ulx -r -------------------------------------------------------------------------------- /tests/runtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | TESTDIR="$(dirname "$0")" 6 | 7 | while [[ "$#" -gt 0 ]]; do 8 | case $1 in 9 | -d|--disassemble) DISASSEMBLE=1; ;; 10 | -f|--file) FILE="$2"; shift ;; 11 | -r|--rem) REM=1; ;; 12 | -s|--safe-funcs) SAFE_FUNCS="--safe-function-overrides=$2"; shift ;; 13 | --stack) STACK="--stack-size=$2"; shift ;; 14 | --stop-on-string) STOP_ON_STRING="--stop-on-string"; ;; 15 | -u|--unsafe-funcs) UNSAFE_FUNCS="--unsafe-function-overrides=$2"; shift ;; 16 | *) echo "Unknown parameter passed: $1"; exit 1 ;; 17 | esac 18 | shift 19 | done 20 | 21 | if [ "$DISASSEMBLE" ]; then 22 | OUTDIR="$PWD/$FILE.disassembled" 23 | DISFLAG="--disassemble" 24 | else 25 | OUTDIR="$PWD/$FILE.decompiled" 26 | fi 27 | 28 | if [ -f "$FILE.gameinfo.dbg" ]; then 29 | DEBUG="--debug-file=$FILE.gameinfo.dbg" 30 | fi 31 | 32 | cargo run --bin glulxtoc -- $FILE --out-dir=$OUTDIR $DISFLAG $DEBUG $SAFE_FUNCS $STACK $STOP_ON_STRING $UNSAFE_FUNCS 33 | 34 | if [ "$REM" ]; then 35 | GLKLIB="remglk" 36 | REMFLAG="-r" 37 | else 38 | GLKLIB="cheapglk" 39 | BINFLAG="-u" 40 | fi 41 | 42 | BUILDDIR="$OUTDIR/$GLKLIB" 43 | mkdir -p $BUILDDIR 44 | export CC=clang 45 | cmake -DGlkLibPath=$TESTDIR/$GLKLIB -B$BUILDDIR -S$OUTDIR 46 | make -C $BUILDDIR -j$(nproc) --no-print-directory 47 | 48 | REGTEST="$TESTDIR/regtest.py" 49 | BIN="$BUILDDIR/$(basename ${FILE%%.*}) $BINFLAG" 50 | TESTFILE="$FILE.regtest" 51 | echo "Running testfile $TESTFILE" 52 | python $REGTEST -i "$BIN" $TESTFILE $REMFLAG -t 10 -------------------------------------------------------------------------------- /tools/reduce-debug-xml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove information from the gameinfo.dbg that we don't need 4 | 5 | pattern="" 6 | pattern+=".+?|" 7 | pattern+=".+?|" 8 | pattern+=".+?|" 9 | pattern+=".+?|" 10 | pattern+=".+?|" 11 | pattern+=".+?|" 12 | pattern+=".+?|" 13 | pattern+=".+?|" 14 | pattern+=".+?|" 15 | pattern+=".+?|" 16 | pattern+=".+?|" 17 | pattern+=".+?|" 18 | pattern+=".+?" 19 | 20 | cat "$1" | perl -pe "s#($pattern)##g" > "$1.reduced" --------------------------------------------------------------------------------