Compare commits
5
Commits
eca9c3236d
...
676856dfe1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
676856dfe1 | ||
|
|
c82a0b4e3c | ||
|
|
ca5915bf31 | ||
|
|
015be8f49a | ||
|
|
2a7a20b498 |
@@ -0,0 +1,5 @@
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = [
|
||||
"-C", "link-arg=-undefined",
|
||||
"-C", "link-arg=dynamic_lookup",
|
||||
]
|
||||
Generated
+79
-68
@@ -190,11 +190,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.17"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -243,11 +244,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.73"
|
||||
version = "1.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||
checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -452,7 +456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"libloading 0.7.3",
|
||||
"libloading 0.8.9",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@@ -478,7 +482,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.7.3",
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -493,6 +497,12 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -509,6 +519,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -714,7 +730,7 @@ dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"com",
|
||||
"libc",
|
||||
"libloading 0.7.3",
|
||||
"libloading 0.8.9",
|
||||
"thiserror",
|
||||
"widestring",
|
||||
"winapi",
|
||||
@@ -767,15 +783,6 @@ dependencies = [
|
||||
"hashbrown 0.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.5"
|
||||
@@ -898,11 +905,10 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
@@ -915,24 +921,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lua-src"
|
||||
version = "544.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "708ba3c844d5e9d38def4a09dd871c17c370f519b3c4b7261fbabe4a613a814c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "luajit-src"
|
||||
version = "210.3.4+resty073ac54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "640b09e99575a442b4da0ef406a78188a1a4313bb9ead7b5b20ec12cc480130f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
@@ -980,30 +968,41 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "mlua"
|
||||
version = "0.8.0-beta.4"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2403730bdaac13f5e84034a324b61723d5d540b85aebc0f237e0028eb58053"
|
||||
checksum = "d96e5d00f19d8c46c71ceaced99593b90c31c57aa1fe2cb3e93a8b1698eedba9"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"cc",
|
||||
"lua-src",
|
||||
"luajit-src",
|
||||
"either",
|
||||
"libc",
|
||||
"mlua-sys",
|
||||
"mlua_derive",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rustc-hash 2.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua-sys"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b806d7ade031f5d6607eae3e283fb034cb795a76247dd0d1ba753c8c42debccf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mlua_derive"
|
||||
version = "0.8.0-beta.1"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd3dfb62b95873e3d7aa9a691905287992628e3c10cadce5ed88f15134e3b0ce"
|
||||
checksum = "87de1e4bdfaa4bb99d8c1449d8a34b0f19e649ef68eb5d4e8dbf1e9ec6f4a084"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.92",
|
||||
"syn 2.0.108",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1019,7 +1018,7 @@ dependencies = [
|
||||
"indexmap 2.12.0",
|
||||
"log",
|
||||
"num-traits",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"spirv",
|
||||
"termcolor",
|
||||
"thiserror",
|
||||
@@ -1368,27 +1367,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.5"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall 0.2.13",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1640,15 +1637,6 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@@ -1696,6 +1684,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
@@ -1830,6 +1824,12 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
@@ -1994,6 +1994,12 @@ dependencies = [
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyjson"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.9"
|
||||
@@ -2127,6 +2133,8 @@ name = "vlua"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mlua",
|
||||
"novaforth",
|
||||
"tinyjson",
|
||||
"vcore",
|
||||
]
|
||||
|
||||
@@ -2135,6 +2143,9 @@ name = "vtest"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"forge_core",
|
||||
"lazy_static",
|
||||
"novaforth",
|
||||
"tinyjson",
|
||||
"vasm_core",
|
||||
"vcore",
|
||||
]
|
||||
@@ -2409,7 +2420,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"web-sys",
|
||||
@@ -2441,7 +2452,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"khronos-egl",
|
||||
"libc",
|
||||
"libloading 0.7.3",
|
||||
"libloading 0.8.9",
|
||||
"log",
|
||||
"metal",
|
||||
"naga",
|
||||
@@ -2453,7 +2464,7 @@ dependencies = [
|
||||
"range-alloc",
|
||||
"raw-window-handle",
|
||||
"renderdoc-sys",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
|
||||
+2
-1
@@ -32,7 +32,8 @@ fn main() {
|
||||
symfile.write(json.as_bytes()).expect("Unable to write to symbol file");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", e)
|
||||
eprintln!("{}", e);
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -2,7 +2,7 @@ mod keyboard;
|
||||
|
||||
use winit::{
|
||||
dpi::LogicalSize,
|
||||
event::{Event, WindowEvent},
|
||||
event::WindowEvent,
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ use vasm_core::assemble_snippet;
|
||||
use vcore::cpu::CPU;
|
||||
use vcore::memory::{Memory, PeekPoke};
|
||||
use vcore::word::Word;
|
||||
use winit::event::{DeviceEvent, DeviceId, ElementState, StartCause};
|
||||
use winit::event::{ElementState, StartCause};
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::keyboard::PhysicalKey;
|
||||
use winit::window::{Window, WindowId};
|
||||
@@ -102,7 +102,7 @@ impl<'a> ApplicationHandler for App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ struct DisplayRegisters {
|
||||
width: Word,
|
||||
row_offset: Word,
|
||||
col_offset: Word,
|
||||
blank_color: Word,
|
||||
}
|
||||
|
||||
impl Default for DisplayRegisters {
|
||||
@@ -25,6 +26,7 @@ impl Default for DisplayRegisters {
|
||||
width: Word::from(40),
|
||||
row_offset: Word::from(0),
|
||||
col_offset: Word::from(0),
|
||||
blank_color: Word::from(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +41,7 @@ fn read_display_registers<P: PeekPoke>(machine: &P, start: Word) -> DisplayRegis
|
||||
width: machine.peek24(start + 13),
|
||||
row_offset: machine.peek24(start + 16),
|
||||
col_offset: machine.peek24(start + 19),
|
||||
blank_color: machine.peek24(start + 22),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +55,7 @@ fn init_display_registers<P: PeekPoke>(machine: &mut P, start: Word) {
|
||||
machine.poke24(start + 13, dr.width);
|
||||
machine.poke24(start + 16, dr.row_offset);
|
||||
machine.poke24(start + 19, dr.col_offset);
|
||||
machine.poke24(start + 22, dr.blank_color);
|
||||
}
|
||||
|
||||
fn init_font<P: PeekPoke>(machine: &mut P) {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod display;
|
||||
mod keyboard;
|
||||
|
||||
|
||||
+3
-1
@@ -10,4 +10,6 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
vcore = { path = "../vcore" }
|
||||
mlua = { version = "0.8.0-beta.4", features = ["lua54", "vendored", "module"] }
|
||||
novaforth = { path = "../novaforth" }
|
||||
tinyjson = "2.5.1"
|
||||
mlua = { version = "0.12.1", features = ["lua55", "module"] }
|
||||
|
||||
+39
-3
@@ -1,21 +1,48 @@
|
||||
use std::collections::HashMap;
|
||||
use mlua::prelude::*;
|
||||
use vcore::{ CPU, Word };
|
||||
use mlua::{UserData, UserDataMethods};
|
||||
use mlua::{Error, UserData, UserDataMethods};
|
||||
use vcore::memory::{PeekPoke, PeekPokeExt};
|
||||
use std::iter::FromIterator;
|
||||
use tinyjson::JsonValue;
|
||||
use novaforth::{ROM, SYMBOLS};
|
||||
use vcore::opcodes::Opcode;
|
||||
|
||||
#[mlua::lua_module]
|
||||
fn libvlua(lua: &Lua) -> LuaResult<LuaTable<'_>> {
|
||||
fn libvlua(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let exports = lua.create_table()?;
|
||||
|
||||
let cpu_constructor = lua.create_function(|_, _: ()| Ok(LuaCPU(CPU::new_random())))?;
|
||||
exports.set("new", cpu_constructor)?;
|
||||
|
||||
let symbols: HashMap<_, _> = {
|
||||
let symbols: JsonValue = SYMBOLS.parse().unwrap();
|
||||
symbols.try_into().unwrap()
|
||||
};
|
||||
|
||||
let symbol_addr = lua.create_function(move |_, sym: String| -> Result<i32, Error> {
|
||||
if let Some(JsonValue::Number(num)) = symbols.get(&sym) {
|
||||
return Ok(*num as i32);
|
||||
}
|
||||
Err(Error::runtime("Symbol not found"))
|
||||
})?;
|
||||
exports.set("symbol", symbol_addr)?;
|
||||
|
||||
let opcode_for = lua.create_function(|_, mnemonic: String| -> Result<u8, _> {
|
||||
if let Ok(op) = Opcode::try_from(mnemonic.as_str()) {
|
||||
return Ok(op.into())
|
||||
}
|
||||
Err(Error::runtime("Invalid opcode"))
|
||||
})?;
|
||||
exports.set("opcode_for", opcode_for)?;
|
||||
|
||||
Ok(exports)
|
||||
}
|
||||
|
||||
struct LuaCPU(CPU);
|
||||
|
||||
impl UserData for LuaCPU {
|
||||
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
fn add_methods<'lua, M: UserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_method_mut("reset", |_, lcpu, ()| {
|
||||
lcpu.0.reset();
|
||||
Ok(())
|
||||
@@ -129,5 +156,14 @@ impl UserData for LuaCPU {
|
||||
lcpu.0.interrupt(irq as usize, arg.map(|n| Word::from(n)));
|
||||
Ok(())
|
||||
});
|
||||
|
||||
methods.add_method_mut("load_rom", |_, lcpu, ()| {
|
||||
for (i, b) in ROM.iter().enumerate() {
|
||||
lcpu.0.poke(Word::from(0x400 + i), *b)
|
||||
}
|
||||
|
||||
lcpu.0.set_pc(0x400.into());
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,3 +9,6 @@ edition = "2021"
|
||||
vcore = { path = "../vcore" }
|
||||
vasm_core = { path = "../vasm_core" }
|
||||
forge_core = { path = "../forge_core" }
|
||||
novaforth = { path = "../novaforth" }
|
||||
lazy_static = "1.4.0"
|
||||
tinyjson = "2.5.1"
|
||||
@@ -0,0 +1,22 @@
|
||||
use std::collections::HashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use tinyjson::JsonValue;
|
||||
use vcore::Word;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SYMBOLS: HashMap<String, Word> = {
|
||||
let symbols: JsonValue = novaforth::SYMBOLS.parse().unwrap();
|
||||
let mut cast = HashMap::new();
|
||||
if let Ok(JsonValue::Object(map)) = symbols.try_into() {
|
||||
for (sym, val) in map {
|
||||
if let JsonValue::Number(f) = val {
|
||||
cast.insert(sym, Word::from(f as u32));
|
||||
}
|
||||
}
|
||||
}
|
||||
cast
|
||||
};
|
||||
}
|
||||
|
||||
pub const TIB: u32 = 80000;
|
||||
pub const SCREEN: u32 = 0x10000;
|
||||
@@ -3,3 +3,15 @@ mod integration_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod forge_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod novaforth_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod memory_item;
|
||||
|
||||
#[cfg(test)]
|
||||
mod constants;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_harness;
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::fmt::Display;
|
||||
use vcore::opcodes::Opcode;
|
||||
use vcore::{Word, CPU};
|
||||
use vcore::memory::PeekPokeExt;
|
||||
use crate::constants::SYMBOLS;
|
||||
|
||||
pub enum MemoryItem {
|
||||
/// A string, null-terminated
|
||||
String(String),
|
||||
/// A pointer to somewhere
|
||||
Pointer(PointerTarget),
|
||||
/// A literal word
|
||||
Value(Word),
|
||||
/// An instruction, maybe containing an argument
|
||||
Instruction(Opcode, Option<Box<MemoryItem>>),
|
||||
/// An opcode, not including the arg length flags that an instruction has
|
||||
Opcode(Opcode),
|
||||
/// Skip some stuff we don't want to both asserting
|
||||
Skip(u32)
|
||||
}
|
||||
|
||||
impl Display for MemoryItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MemoryItem::String(s) => write!(f, "str({})", s),
|
||||
MemoryItem::Pointer(p) => write!(f, "ptr({})", p),
|
||||
MemoryItem::Value(v) => write!(f, "num({})", v),
|
||||
MemoryItem::Instruction(opcode, Some(arg)) => write!(f, "inst({}, {})", opcode, arg),
|
||||
MemoryItem::Instruction(opcode, None) => write!(f, "inst({})", opcode),
|
||||
MemoryItem::Opcode(opcode) => write!(f, "{}", opcode),
|
||||
MemoryItem::Skip(len) => write!(f, "skip({})", len),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryItem {
|
||||
/// Asserts that this heap item is found at the given offset from the heap ptr in the given CPU
|
||||
pub fn check<W: Into<Word>>(&self, cpu: &CPU, base_sym: &str, offset: W) -> Result<(), ()> {
|
||||
let base: u32 = SYMBOLS[base_sym].into();
|
||||
let offset: u32 = offset.into().into();
|
||||
match self {
|
||||
&MemoryItem::String(ref expected) => {
|
||||
let mut actual = String::with_capacity(expected.len());
|
||||
let mut curr = base + offset;
|
||||
while cpu.peek8(curr) != 0 {
|
||||
actual.push(cpu.peek8(curr) as char);
|
||||
curr += 1
|
||||
}
|
||||
|
||||
if expected != &actual { Err(()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
&MemoryItem::Pointer(ref expected) => {
|
||||
let expected = expected.addr(cpu);
|
||||
let actual: u32 = cpu.peek24(base + offset).into();
|
||||
if expected != actual { Err(()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
&MemoryItem::Instruction(ref opcode, ref arg) => {
|
||||
let actual_op = cpu.peek8(base + offset);
|
||||
if *opcode != Opcode::try_from(actual_op / 4).unwrap() { return Err(()) }
|
||||
if let Some(arg) = arg {
|
||||
if actual_op & 0x3 != 3 as u8 { Err(()) } else {
|
||||
arg.check(cpu, base_sym, offset + 1)
|
||||
}
|
||||
} else {
|
||||
if actual_op & 0x3 != 0 { Err(()) } else { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
&MemoryItem::Opcode(ref opcode) => {
|
||||
let actual_val = cpu.peek8(base + offset);
|
||||
if u8::from(*opcode) == actual_val { Ok(()) } else { Err(()) }
|
||||
}
|
||||
|
||||
&MemoryItem::Value(ref val) => {
|
||||
let actual = cpu.peek24(base + offset);
|
||||
if *val != actual { Err(()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
&MemoryItem::Skip(_) => { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> u32 {
|
||||
match self {
|
||||
MemoryItem::String(s) => s.len() as u32 + 1, // Add the null terminator
|
||||
MemoryItem::Pointer(_) | MemoryItem::Value(_) => 3, // Any pointer is 3 long
|
||||
MemoryItem::Instruction(_, Some(_)) => 4, // Any instruction with an arg
|
||||
MemoryItem::Instruction(_, None) => 1, // No arg
|
||||
MemoryItem::Opcode(_) => 1,
|
||||
MemoryItem::Skip(size) => *size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes(&self, cpu: &CPU) -> Vec<u8> {
|
||||
match self {
|
||||
&MemoryItem::String(ref s) => s.as_bytes().to_vec(),
|
||||
&MemoryItem::Pointer(ref p) => Vec::from(p.addr(cpu).to_bytes()),
|
||||
MemoryItem::Instruction(op, Some(arg)) => { // Any instruction with an arg
|
||||
let mut v = vec![u8::from(*op) * 4 + arg.len() as u8];
|
||||
v.extend(arg.bytes(cpu));
|
||||
v
|
||||
},
|
||||
MemoryItem::Opcode(op) => vec![u8::from(*op)],
|
||||
MemoryItem::Instruction(op, None) => vec![u8::from(*op) * 4], // No arg
|
||||
&MemoryItem::Value(ref v) => Vec::from(v.to_bytes()),
|
||||
&MemoryItem::Skip(_) => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PointerTarget {
|
||||
/// An absolute address
|
||||
Absolute(Word),
|
||||
/// The address of a symbol
|
||||
Symbol(String),
|
||||
/// An offset from the start of the heap
|
||||
Heap(Word),
|
||||
/// Whatever the new heap pointer is
|
||||
NewHeap,
|
||||
}
|
||||
|
||||
impl Display for PointerTarget {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PointerTarget::Absolute(a) => write!(f, "<{}>", a),
|
||||
PointerTarget::Symbol(s) => write!(f, "<{}: {}>", s, SYMBOLS[s]),
|
||||
PointerTarget::Heap(h) => write!(f, "<heap + {}: {}>", h, SYMBOLS["heap_start"] + *h),
|
||||
PointerTarget::NewHeap => write!(f, "<newheap>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Word> for PointerTarget {
|
||||
fn from(word: Word) -> Self { Self::Absolute(word) }
|
||||
}
|
||||
|
||||
impl From<&str> for PointerTarget {
|
||||
fn from(word: &str) -> Self { Self::Symbol(word.to_string()) }
|
||||
}
|
||||
|
||||
pub fn ascii(s: &str) -> MemoryItem { MemoryItem::String(s.to_owned()) }
|
||||
pub fn ptr<P: Into<PointerTarget>>(val: P) -> MemoryItem { MemoryItem::Pointer(val.into()) }
|
||||
pub fn heap<W: Into<Word>>(val: W) -> PointerTarget { PointerTarget::Heap(val.into()) }
|
||||
pub fn new_heap() -> PointerTarget { PointerTarget::NewHeap}
|
||||
pub fn op(mnemonic: &str) -> MemoryItem { MemoryItem::Opcode(Opcode::try_from(mnemonic).unwrap()) }
|
||||
pub fn num<W: Into<Word>>(val: W) -> MemoryItem { MemoryItem::Value(val.into()) }
|
||||
pub fn inst4<H: Into<MemoryItem>>(mnemonic: &str, arg: H) -> MemoryItem { MemoryItem::Instruction(Opcode::try_from(mnemonic).unwrap(), Some(Box::new(arg.into()))) }
|
||||
pub fn inst1(mnemonic: &str) -> MemoryItem { MemoryItem::Instruction(Opcode::try_from(mnemonic).unwrap(), None) }
|
||||
pub fn skip(size: u32) -> MemoryItem { MemoryItem::Skip(size) }
|
||||
|
||||
impl Into<MemoryItem> for i32 {
|
||||
fn into(self) -> MemoryItem { MemoryItem::Value(self.into()) }
|
||||
}
|
||||
|
||||
impl PointerTarget {
|
||||
pub fn addr(&self, cpu: &CPU) -> Word {
|
||||
match self {
|
||||
&Self::Absolute(addr) => addr,
|
||||
&Self::Symbol(ref name) => SYMBOLS[name],
|
||||
&Self::Heap(offset) => SYMBOLS["heap_start"] + offset,
|
||||
&Self::NewHeap => cpu.peek24(SYMBOLS["heap"]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,712 @@
|
||||
use memory_item::{ascii, heap, inst1, inst4, num, op, ptr, skip};
|
||||
use crate::constants::SYMBOLS;
|
||||
use crate::memory_item;
|
||||
use crate::memory_item::PointerTarget;
|
||||
use crate::test_harness::{init_cpu, TestHarness};
|
||||
|
||||
/// TODO:
|
||||
/// - `quit` should clear the rstack but not the data stack, new opcode probably
|
||||
/// - refactor test assert fns to be shorter / in a different file
|
||||
///
|
||||
/// Later TODO:
|
||||
/// - Remove 'continue', we can implement it ourselves easily
|
||||
/// - Prelude of simple words
|
||||
/// - Rewrite / macro-ize string fns
|
||||
|
||||
#[test]
|
||||
fn test_dupnz() {
|
||||
init_cpu().given_stack([3]).test_fn("dupnz").expect_stack([3, 3]);
|
||||
init_cpu().given_stack([0]).test_fn("dupnz").expect_stack([0]);
|
||||
|
||||
init_cpu().test_line("10 ?dup").expect_stack([10, 10]);
|
||||
init_cpu().test_line("0 ?dup").expect_stack([0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_parsing() {
|
||||
init_cpu().test_line("10").expect_stack([10]);
|
||||
init_cpu().test_line("10 20 30").expect_stack([10, 20, 30]).expect_empty_rstack();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_fail() {
|
||||
init_cpu().test_line("notaword").expect_output("Not a word: notaword\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create() {
|
||||
// Should create a new dictionary entry:
|
||||
init_cpu().test_line("create blah").expect_heap([
|
||||
ascii("blah"), // Name
|
||||
ptr(memory_item::new_heap()), // Points to right after the entry
|
||||
ptr("dict_start") // Points to the old dict head
|
||||
]).expect_pointer("dictionary", heap(0)); // Dict has the new entry consed on to it
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entering_exiting_immediate_mode() {
|
||||
init_cpu().test_line("]").expect_pointer("handleword_hook", "compile_handleword");
|
||||
init_cpu().test_line("] [").expect_pointer("handleword_hook", "immediate_handleword").expect_empty_rstack();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_compilation() {
|
||||
// Compiling a number
|
||||
init_cpu().test_line("] 122773").expect_heap([
|
||||
inst4("push", 122773)
|
||||
]);
|
||||
|
||||
// Compiling a call
|
||||
init_cpu().test_line("] create").expect_heap([
|
||||
inst4("call", ptr("nova_create"))
|
||||
]);
|
||||
|
||||
// Compiling gibberish
|
||||
init_cpu().test_line("] stillnotaword").expect_output("Not a word: stillnotaword\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_continue() {
|
||||
// Continue word (compiles a jmp)
|
||||
init_cpu().test_line("] continue ]").expect_heap([
|
||||
inst4("jmp", ptr("nova_close_bracket"))
|
||||
]);
|
||||
|
||||
// Continue compile word
|
||||
init_cpu().test_line("] continue [").expect_heap([
|
||||
inst4("jmp", ptr("nova_open_bracket"))
|
||||
]);
|
||||
|
||||
// Continue gibberish
|
||||
init_cpu().test_line("] continue supernotword").expect_output("Not a word: supernotword\n");
|
||||
|
||||
// Implement continue with #asm!
|
||||
init_cpu()
|
||||
.test_line(": cont ' $ jmp #asm ; immediate")
|
||||
.test_line("] cont ]")
|
||||
.expect_heap([
|
||||
skip(11), // Just skip cont's header
|
||||
inst4("call", ptr("nova_tick")), // Call tick to see what we're continuing to
|
||||
inst4("push", op("jmp")), // Push a jmp
|
||||
inst4("call", ptr("compile_instruction_arg")), // Compile a jmp to that word
|
||||
inst1("ret"), // Return from cont
|
||||
inst4("jmp", ptr("nova_close_bracket")), // Cont gives us a jmp to `]`
|
||||
]);
|
||||
|
||||
// Prelude continue with a runtime word
|
||||
init_cpu()
|
||||
.test_line(": cont ' $ jmp #asm ; immediate")
|
||||
.test_line("] cont print")
|
||||
.expect_heap([
|
||||
skip(11 + 13), // Just skip cont's header and impl
|
||||
inst4("jmp", ptr("print")) // Cont gives us a jmp to `print`
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prelude_colon() {
|
||||
// Prelude colon definition
|
||||
init_cpu()
|
||||
.test_line(": cont ' $ jmp #asm ; immediate")
|
||||
.test_line("create :: ] create cont ] [")
|
||||
.expect_heap([
|
||||
skip(24), // 24 bytes for cont
|
||||
ascii("::"), // New dict entry has the name
|
||||
ptr(heap(24 + 9)), // Followed by the ptr to the fn
|
||||
skip(3), // Pointer to dict start
|
||||
inst4("call", ptr("nova_create")), // Which is a call to create...
|
||||
inst4("jmp", ptr("nova_close_bracket")), // Followed by jmping to close_bracket
|
||||
])
|
||||
.expect_pointer("handleword_hook", "immediate_handleword") // And now we're back in immediate mode
|
||||
.expect_empty_rstack(); // And haven't leaked a stack frame
|
||||
|
||||
init_cpu()
|
||||
.test_line("create cont ] ' $ jmp #asm ; immediate")
|
||||
.test_line("create :: ] create cont ] [ :: foo 35")
|
||||
.expect_heap([
|
||||
skip(24 + 17), // Skip cont and ::
|
||||
ascii("foo"), // A new entry for foo
|
||||
ptr(heap(24 + 27)), // Defn ptr is right after this
|
||||
skip(3),
|
||||
inst4("push", 35), // fn begins with pushing a 35
|
||||
])
|
||||
.expect_pointer("handleword_hook", "compile_handleword")
|
||||
.expect_empty_rstack();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_postpone() {
|
||||
// Postponing normal words
|
||||
init_cpu().test_line("] postpone create").expect_heap([
|
||||
inst4("push", ptr("nova_create")),
|
||||
inst4("push", op("call")),
|
||||
inst4("call", ptr("compile_instruction_arg"))
|
||||
]);
|
||||
|
||||
// Postponing compile words
|
||||
init_cpu().test_line("] postpone [").expect_heap([
|
||||
inst4("call", ptr("nova_open_bracket")),
|
||||
]);
|
||||
|
||||
// Postponing gibberish
|
||||
init_cpu().test_line("] postpone reallynotaword").expect_output("Not a word: reallynotaword\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exit() {
|
||||
// Compile a ret
|
||||
init_cpu().test_line("] exit").expect_heap([
|
||||
inst1("ret")
|
||||
]);
|
||||
}
|
||||
|
||||
/// This was a fun intellectual exercise and makes a nice torture test for NovaForth, but it violates the
|
||||
/// "optimize for understandability" principle and so colon and semicolon are now both written in asm. The
|
||||
/// tests remain here because they're good, very exhaustive, tests.
|
||||
#[test]
|
||||
fn test_prelude() {
|
||||
// Implementing colon and semicolon in Forth itself
|
||||
let p1 = "create :: ] create continue ] [";
|
||||
let p2 = ":: ;; postpone exit continue [ [ immediate";
|
||||
let psize = 34;
|
||||
|
||||
// Prelude semicolon definition
|
||||
init_cpu().test_line(p1).test_line(p2)
|
||||
.expect_heap([
|
||||
skip(17), ascii(";;"), skip(3), ptr("compile_dict_start"), // A new entry for semicolon
|
||||
inst4("call", ptr("nova_exit")), // Which compiles a ret
|
||||
inst4("jmp", ptr("nova_open_bracket")), // And then returns to immediate mode
|
||||
])
|
||||
.expect_pointer("compile_dictionary", PointerTarget::Heap(17.into())) // Semicolon is in the compile dict
|
||||
.expect_pointer("handleword_hook", "immediate_handleword"); // In immediate mode again
|
||||
|
||||
// Using prelude semicolon
|
||||
init_cpu().test_line(p1).test_line(p2).test_line("] ;;")
|
||||
.expect_heap([
|
||||
skip(psize),
|
||||
inst1("ret") // Compiled our ret
|
||||
])
|
||||
.expect_pointer("handleword_hook", "immediate_handleword") // In immediate mode again
|
||||
.expect_empty_rstack();
|
||||
|
||||
// Defining a word and calling it, with the prelude
|
||||
init_cpu().test_line(p1).test_line(p2).test_line(":: fives 5 5 5 ;; fives")
|
||||
.expect_stack([5, 5, 5])
|
||||
.expect_empty_rstack();
|
||||
|
||||
// Testing create / does> without compile-time behavior, with the prelude
|
||||
init_cpu().test_line(p1).test_line(p2).test_line(":: blah create does> 2 3 ;; blah fnord fnord")
|
||||
// We're creating a new word fnord and then running it, the new word gets passed the address
|
||||
// of its heap stuff and then pushes a couple numbers. Its heap area is the heap ptr when we
|
||||
// called does>, so, PRELUDE + 11 (blah's entry) + 21 (blah's body, part of which is fnord's) + 12 (fnord's entry)
|
||||
.expect_stack([u32::from(SYMBOLS["heap_start"]) + psize + 11 + 22 + 12, 2, 3])
|
||||
.expect_heap([
|
||||
skip(psize + 11), // Skip prelude and blah's header
|
||||
// Body of blah:
|
||||
inst4("call", ptr("nova_create")), // After blah's header, we have a call to create
|
||||
inst4("push", ptr(heap(psize + 11 + 13))), // push the address of after the does>
|
||||
inst4("jmp", ptr("does_at_runtime")), // And a call to does@runtime, to start compiling it
|
||||
inst1("ret"), // blah's return
|
||||
inst4("push", 2), // The runtime behavior of fnord (the "mold"):
|
||||
inst4("push", 3),
|
||||
inst1("ret"), // fnord's runtime return
|
||||
|
||||
// Header of fnord:
|
||||
ascii("fnord"), // the new word's header
|
||||
ptr(heap(psize + 11 + 22 + 12)), // pointer to the trampoline
|
||||
// and pointer to the next dictionary entry. By this point the front of the
|
||||
// dictionary is blah, which has its entry at heap(psize), right after the prelude:
|
||||
ptr(heap(psize)),
|
||||
|
||||
// Body (trampoline) of fnord:
|
||||
// Push the old value, which was right after the header (because of the null compile-time behavior)
|
||||
inst4("push", ptr(heap(psize + 11 + 22 + 12))),
|
||||
inst4("jmp", ptr(heap(psize + 11 + 13))) // jmp to the runtime behavior, after the does> call
|
||||
]);
|
||||
|
||||
// Testing create / does> when there's compile-time behavior, with the prelude
|
||||
init_cpu().test_line(p1).test_line(p2).test_line(":: blah create 15 , does> 3 ;; blah fnord fnord")
|
||||
// We're creating a new word fnord and then running it, the new word gets passed the address
|
||||
// of its heap stuff and then pushes a three. Its heap area is the heap ptr when we
|
||||
// called does>, so, psize + 11 (blah's entry) + 26 (blah's body, part of which is fnord's) + 12 (fnord's entry)
|
||||
.expect_stack([u32::from(SYMBOLS["heap_start"]) + psize + 11 + 26 + 12, 3])
|
||||
.expect_heap([
|
||||
skip(psize + 11), // Skip prelude and blah's header
|
||||
// Body of blah:
|
||||
inst4("call", ptr("nova_create")), // After blah's header, we have a call to create
|
||||
inst4("push", 15),
|
||||
inst4("call", ptr("nova_comma")),
|
||||
inst4("push", ptr(heap(psize + 11 + 21))), // push the address of after the does>
|
||||
inst4("jmp", ptr("does_at_runtime")), // And a call to does@runtime, to start compiling it
|
||||
inst1("ret"), // blah's return
|
||||
inst4("push", 3), // The runtime behavior of fnord (the "mold"):
|
||||
inst1("ret"), // fnord's runtime return
|
||||
|
||||
// Header of fnord:
|
||||
ascii("fnord"), // the new word's header
|
||||
ptr(heap(psize + 11 + 26 + 15)), // pointer to the trampoline
|
||||
// and pointer to the next dictionary entry. By this point the front of the
|
||||
// dictionary is blah, which has its entry at heap(psize), right after the prelude:
|
||||
ptr(heap(psize)),
|
||||
|
||||
num(15), // The compile time behavior compiled this 15
|
||||
|
||||
// Body (trampoline) of fnord:
|
||||
// Push the old value, which was right after the header and the 15 we compiled
|
||||
inst4("push", ptr(heap(psize + 11 + 26 + 12))),
|
||||
inst4("jmp", ptr(heap(psize + 11 + 21))) // jmp to the runtime behavior, after the does> call
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normal_define() {
|
||||
// Defining a word and calling it, with the normal colon / semicolon words
|
||||
init_cpu().test_line(": fives 5 5 5 ; fives").expect_stack([5, 5, 5]).expect_empty_rstack();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_asm() {
|
||||
// Basic use of asm
|
||||
init_cpu().test_line("create execute $ jmp asm").expect_heap([
|
||||
ascii("execute"),
|
||||
ptr(heap(14)),
|
||||
skip(3),
|
||||
inst1("jmp")
|
||||
]);
|
||||
|
||||
// Asm with args
|
||||
init_cpu().test_line("45 $ push #asm").expect_heap([
|
||||
inst4("push", 45)
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_mode_asm() {
|
||||
// Compile-mode asm
|
||||
init_cpu().test_line("] $ jmp asm").expect_heap([
|
||||
inst4("push", op("jmp")),
|
||||
inst4("call", ptr("compile_instruction")),
|
||||
]);
|
||||
|
||||
// Compile-mode asm with args
|
||||
init_cpu().test_line("] 45 $ xor #asm").expect_heap([
|
||||
inst4("push", 45),
|
||||
inst4("push", op("xor")),
|
||||
inst4("call", ptr("compile_instruction_arg"))
|
||||
]);
|
||||
|
||||
init_cpu().test_line(": foo 34 $ xor #asm ; immediate ] foo").expect_heap([
|
||||
// Foo's header
|
||||
ascii("foo"), ptr(heap(10)), ptr("compile_dict_start"),
|
||||
inst4("push", 34), // Push an arg
|
||||
inst4("push", op("xor")), // Push an opcode
|
||||
inst4("call", ptr("compile_instruction_arg")), // Compile that with an arg
|
||||
inst1("ret"), // Return from foo
|
||||
// Foo is now an immediate word, and when we call it in compile mode...
|
||||
inst4("xor", 34) // It compiles a xor 34
|
||||
]);
|
||||
|
||||
init_cpu().test_line("$ xor 3").expect_stack([9, 3]);
|
||||
|
||||
init_cpu().test_line("$ blah 3").expect_empty_stack().expect_output("Invalid mnemonic: blah\n");
|
||||
|
||||
init_cpu().test_line("] $ xor 3").expect_empty_stack().expect_heap([
|
||||
inst4("push", 9),
|
||||
inst4("push", 3)
|
||||
]);
|
||||
|
||||
init_cpu().test_line("] $ blah 3")
|
||||
.expect_empty_stack()
|
||||
.expect_output("Invalid mnemonic: blah\n")
|
||||
.expect_pointer("heap", "heap_start"); // It hits quit right after the error
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comma_compile() {
|
||||
// Comma compile a number
|
||||
init_cpu().test_line("1234 ,").expect_heap([num(1234)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tick() {
|
||||
// Tick a word
|
||||
init_cpu().test_line("' print").expect_stack([SYMBOLS["print"]]);
|
||||
|
||||
// Bracket-tick a word
|
||||
init_cpu().test_line("] ['] print")
|
||||
.expect_heap([inst4("push", ptr("print"))]);
|
||||
|
||||
// Tick gibberish
|
||||
init_cpu().test_line("' bananas")
|
||||
.expect_empty_stack()
|
||||
.expect_empty_rstack()
|
||||
.expect_output("Not a word: bananas\n");
|
||||
|
||||
// Bracket-tick gibberish
|
||||
init_cpu().test_line("] ['] penguin")
|
||||
.expect_empty_stack()
|
||||
.expect_empty_rstack()
|
||||
.expect_output("Not a word: penguin\n");
|
||||
|
||||
// Tick a compile word
|
||||
init_cpu().test_line("' [").expect_stack([SYMBOLS["nova_open_bracket"]]);
|
||||
|
||||
// Bracket-tick a compile word
|
||||
init_cpu().test_line("] ['] does>")
|
||||
.expect_heap([inst4("push", ptr("does_word"))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pad() {
|
||||
// Fetch the pad address
|
||||
init_cpu().test_line(" pad ").expect_stack([SYMBOLS["pad"]]);
|
||||
|
||||
// Read a word to the pad
|
||||
init_cpu().test_line("word mango")
|
||||
.expect_output("")
|
||||
.expect_stack([SYMBOLS["pad"]])
|
||||
.expect_pad([ascii("mango")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_literal() {
|
||||
// Literal, compiles a push instruction
|
||||
init_cpu().test_line("1234 ] literal")
|
||||
.expect_empty_stack()
|
||||
.expect_heap([inst4("push", 1234)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comments() {
|
||||
// Paren comments
|
||||
init_cpu().test_line("1 2 ( 3 4 5 ) 6").expect_stack([1, 2, 6]);
|
||||
|
||||
// Nested paren comments
|
||||
init_cpu().test_line("1 2 ( ( 3 4 ) 5 6").expect_stack([1, 2]);
|
||||
|
||||
// Compiled paren comments
|
||||
init_cpu().test_line("] 1 2 ( 3 4 5 ) 6").expect_heap([skip(12)]);
|
||||
|
||||
// Compiled nested paren comments
|
||||
init_cpu().test_line("] 1 2 ( ( 3 4 ) 5 6").expect_heap([skip(8)]);
|
||||
|
||||
// Backslash comments
|
||||
init_cpu().test_line("1 2 \\ 3 4").test_line("5 6").expect_stack([1, 2, 5, 6]);
|
||||
|
||||
// Compiled backslash comments
|
||||
init_cpu().test_line("] 1 2 \\ 3 4").test_line("5 6").expect_heap([skip(16)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_numbers() {
|
||||
// Parse numbers from words
|
||||
init_cpu().test_line("number 17").expect_stack([17, 1]);
|
||||
init_cpu().test_line("number blah").expect_stack([0]);
|
||||
init_cpu().test_line("number -23").expect_stack([-23 & 0xffffff, 1]);
|
||||
|
||||
// Parse hex numbers from words
|
||||
init_cpu().test_line("hex number a4").expect_stack([164, 1]);
|
||||
init_cpu().test_line("hex number blah").expect_stack([0]);
|
||||
|
||||
// Switch between hex and dec
|
||||
init_cpu().test_line("hex number a4 dec number 23").expect_stack([164, 1, 23, 1]);
|
||||
init_cpu().test_line("hex a4 dec 23").expect_stack([164, 23]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_output() {
|
||||
// Output in hex and dec
|
||||
init_cpu().test_line("hex a4 . dec 23 .").expect_output("a423"); // Yeah, no separator
|
||||
init_cpu().test_line("hex a4 dec .").expect_output("164");
|
||||
init_cpu().test_line("dec 525 hex .").expect_output("20d");
|
||||
init_cpu().test_line("-15 .").expect_output("-15");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_strings() {
|
||||
// Compiling strings to the heap
|
||||
init_cpu().test_line("s\" foo\"")
|
||||
.expect_stack([SYMBOLS["heap_start"]])
|
||||
.expect_cursor(7)
|
||||
.expect_heap([ascii("foo")]);
|
||||
|
||||
// Compiling empty string
|
||||
init_cpu().test_line("s\" \"")
|
||||
.expect_stack([SYMBOLS["heap_start"]])
|
||||
.expect_heap([ascii("")]);
|
||||
|
||||
// Unterminated string
|
||||
init_cpu().test_line("s\" foo")
|
||||
.expect_empty_stack()
|
||||
.expect_cursor(6)
|
||||
.expect_output("Unclosed string")
|
||||
.expect_pointer("heap", "heap_start");
|
||||
|
||||
// Compile move squote
|
||||
init_cpu().test_line("] s\" blah\"")
|
||||
.expect_heap([
|
||||
inst4("jmpr", num(9)), // length of the jmpr itself + 'blah\0'
|
||||
ascii("blah"), // The actual string
|
||||
inst4("push", ptr(heap(4))) // Push the addr of the string
|
||||
]);
|
||||
|
||||
// Compile mode unterminated string
|
||||
init_cpu().test_line("] s\" foo")
|
||||
.expect_heap([])
|
||||
.expect_output("Unclosed string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output() {
|
||||
// Basic output
|
||||
init_cpu().test_line(".\" foo\"")
|
||||
.expect_empty_stack().expect_heap([])
|
||||
.expect_output("foo");
|
||||
|
||||
// Compile output
|
||||
init_cpu().test_line("] .\" foo\"")
|
||||
.expect_heap([
|
||||
inst4("jmpr", 8),
|
||||
ascii("foo"),
|
||||
inst4("push", ptr(heap(4))),
|
||||
inst4("call", ptr("print"))
|
||||
]);
|
||||
|
||||
// Unterminated output
|
||||
init_cpu().test_line(".\" foo")
|
||||
.expect_empty_stack().expect_heap([])
|
||||
.expect_output("Unclosed string");
|
||||
|
||||
// Compile output
|
||||
init_cpu().test_line("] .\" foo")
|
||||
.expect_heap([])
|
||||
.expect_output("Unclosed string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_print() {
|
||||
init_cpu().test_line("s\" foo\" print")
|
||||
.expect_cursor(13)
|
||||
.expect_heap([ascii("foo")])
|
||||
.expect_output("foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare() {
|
||||
init_cpu().test_line("s\" foo\" s\" bar\" compare").expect_stack([0]);
|
||||
init_cpu().test_line("s\" foo\" s\" foo\" compare").expect_stack([1]);
|
||||
init_cpu().test_line("s\" foo\" ?dup compare").expect_stack([1]); // There's no simple dup...
|
||||
init_cpu().test_line("s\" foo\" s\" foo234\" compare").expect_stack([0]);
|
||||
init_cpu().test_line("s\" foo123\" s\" foo\" compare").expect_stack([0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_print_stack() {
|
||||
// Print the stack
|
||||
init_cpu().test_line("10 20 30 .s")
|
||||
.expect_stack([ 10, 20, 30 ])
|
||||
.expect_output("<< 10 20 30 >>");
|
||||
|
||||
// Print the stack in hex
|
||||
init_cpu().test_line("10 20 30 hex .s")
|
||||
.expect_stack([ 10, 20, 30 ])
|
||||
.expect_output("<< a 14 1e >>");
|
||||
|
||||
// Print nothing
|
||||
init_cpu().test_line(".s")
|
||||
.expect_empty_stack()
|
||||
.expect_output("<< >>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_4th_rstack() {
|
||||
// pushr, peekr
|
||||
init_cpu().test_line("3 >r r@")
|
||||
.expect_stack([3])
|
||||
.expect_4th_rstack([num(3)]);
|
||||
|
||||
// popr
|
||||
init_cpu().test_line("3 >r 5 r>")
|
||||
.expect_stack([5, 3])
|
||||
.expect_4th_rstack([]);
|
||||
|
||||
// rpick
|
||||
init_cpu().test_line("10 20 30 >r >r >r 2 rpick")
|
||||
.expect_stack([30])
|
||||
.expect_4th_rstack([num(30), num(20), num(10)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heap_ptr() {
|
||||
init_cpu().test_line("&heap").expect_stack([SYMBOLS["heap"]]);
|
||||
init_cpu().test_line("here").expect_stack([SYMBOLS["heap_start"]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_asm_resolve() {
|
||||
// To-asm
|
||||
init_cpu().test_line("$ brnz >asm")
|
||||
.expect_empty_stack()
|
||||
.expect_heap([inst4("brnz", 0)])
|
||||
.expect_4th_rstack([ptr(heap(1))]);
|
||||
|
||||
// Resolve
|
||||
init_cpu().test_line("$ brnz >asm resolve")
|
||||
.expect_4th_rstack([])
|
||||
.expect_heap([inst4("brnz", 4)]); // brnz 12 ahead
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if() {
|
||||
// An 'if' implementation
|
||||
init_cpu().test_line(": if $ brz >asm ; immediate ] if")
|
||||
.expect_empty_stack()
|
||||
.expect_4th_rstack([ptr(heap(9 + 9 + 1))]) // Address of said brnz' arg
|
||||
.expect_heap([
|
||||
skip(9), // Skip if's header
|
||||
inst4("push", op("brz")),
|
||||
inst4("call", ptr("nova_asm_to")),
|
||||
inst1("ret"),
|
||||
inst4("brz", num(0)) // The unresolved brnz 'if' compiled
|
||||
]);
|
||||
|
||||
// If / then
|
||||
init_cpu()
|
||||
.test_line(": if $ brz >asm ; immediate")
|
||||
.test_line(": then resolve ; immediate")
|
||||
.test_line(": foo if 2 then ;")
|
||||
.test_line("1 foo 10 0 foo")
|
||||
.expect_stack([2, 10]);
|
||||
|
||||
// If / else / then
|
||||
init_cpu()
|
||||
.test_line(": if $ brz >asm ; immediate")
|
||||
.test_line(": then resolve ; immediate")
|
||||
.test_line(": else r> $ jmpr >asm >r resolve ; immediate")
|
||||
.test_line(": foo if 2 else 3 then ;")
|
||||
.test_line("1 foo 10 0 foo")
|
||||
.expect_stack([2, 10, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loops() {
|
||||
// Begin / until loops
|
||||
init_cpu()
|
||||
.test_line(": begin here >r ; immediate") // Begin just marks a point in the program we'll brnz back to
|
||||
// Here's the fun part.
|
||||
// Pull the address stored by 'begin' off the rstack and subtract `here` from it
|
||||
// Then compile a brz to that address
|
||||
.test_line(": until r> here - $ brz #asm ; immediate")
|
||||
// This ought to loop from 5..0, leaving each one on the stack
|
||||
.test_line(": foo 5 begin dup 1 - dup not until ; foo")
|
||||
.expect_stack([5, 4, 3, 2, 1, 0]);
|
||||
|
||||
// do / loop counted loops
|
||||
init_cpu()
|
||||
.test_line("create 1+ 1 $ add #asm ] ;")
|
||||
.test_line(": do postpone swap postpone >r postpone >r here >r ; immediate")
|
||||
.test_line(": _loop_test r> 1+ dup r@ < swap >r ;") // pull off and inc the cntr, dup, peek at the limit, compare them, put the new cntr back
|
||||
.test_line(": unloop r> r> pop pop ;")
|
||||
.test_line(": loop postpone _loop_test r> here - $ brnz #asm postpone unloop ; immediate")
|
||||
.test_line(": foo 3 0 do 33 loop ; foo")
|
||||
.expect_stack([33, 33, 33]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quit() {
|
||||
// Testing quit as called by an error
|
||||
init_cpu().test_line("2 3 : foo nooope ; 7")
|
||||
.expect_heap([skip(10)]) // It does the header but that's it
|
||||
.expect_output("Not a word: nooope\n") // Spits out an error message
|
||||
.expect_pointer("handleword_hook", "immediate_handleword") // Back in immediate mode
|
||||
.expect_empty_stack(); // Clobbers the stack
|
||||
|
||||
// Testing quit as called manually
|
||||
init_cpu().test_line(": low 3 quit 65 emit ;")
|
||||
.test_line(": med 2 low 66 emit ;")
|
||||
.test_line(": high 1 med 67 emit ;")
|
||||
.test_line("high")
|
||||
.expect_output("") // This isn't an error, we just quit
|
||||
.expect_empty_stack(); // We quit partway through 'low', so skip all the frames above that
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lambdas() {
|
||||
// Testing immediate-mode lambdas
|
||||
init_cpu().test_line("{ 3 5 }")
|
||||
.expect_output("")
|
||||
.expect_pointer("handleword_hook", "immediate_handleword") // Back in immediate mode
|
||||
.expect_stack([SYMBOLS["heap_start"]]) // Leaves the address of the lambda on the stack
|
||||
.expect_heap([]) // It does not move the heap, but things are stored after the heap ptr, even though it hasn't moved
|
||||
.expect_memory("heap_start",[
|
||||
inst4("push", 3),
|
||||
inst4("push", 5),
|
||||
inst1("ret")
|
||||
]);
|
||||
|
||||
init_cpu().test_line("{ 3 5 } execute").expect_stack([3, 5]); // Runs the anonymous fn
|
||||
|
||||
// Compile-mode lambda, non-nested
|
||||
init_cpu().test_line(": foo 1 { 2 } ; foo")
|
||||
.expect_output("")
|
||||
.expect_heap([
|
||||
skip(10),
|
||||
inst4("push", 1),
|
||||
inst4("jmpr", 4+4+1), // jmpr, push, ret
|
||||
inst4("push", 2),
|
||||
inst1("ret"),
|
||||
inst4("push", ptr(heap(10+4+4))), // header, push(1), jmpr
|
||||
inst1("ret")
|
||||
])
|
||||
.expect_stack([1, u32::from(SYMBOLS["heap_start"]) + 10 + 4 + 4]);
|
||||
|
||||
// Compile-mode lambda, nested
|
||||
init_cpu().test_line(": foo 1 { 2 { 3 } } ; foo")
|
||||
.expect_output("")
|
||||
.expect_heap([
|
||||
skip(10),
|
||||
inst4("push", 1),
|
||||
inst4("jmpr", 4*4 + 1 + 4 + 1), // jmpr, push(2), jmpr, push(3), ret, push(inner-lambda), ret
|
||||
inst4("push", 2),
|
||||
inst4("jmpr", 4+4+1), // inner lambda: jmpr, push, ret
|
||||
inst4("push", 3),
|
||||
inst1("ret"),
|
||||
inst4("push", ptr(heap(10 + 4 * 4))), // push the inner-lambda addr
|
||||
inst1("ret"),
|
||||
inst4("push", ptr(heap(10+4+4))),
|
||||
inst1("ret")
|
||||
])
|
||||
.expect_stack([1, u32::from(SYMBOLS["heap_start"]) + 10 + 4 + 4])
|
||||
.expect_var("lambda_nesting_level", 0);
|
||||
|
||||
// Executing nested compile-mode lambdas
|
||||
init_cpu().test_line(": foo 1 { 2 { 3 } } ; foo execute execute").expect_stack([1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_graham_accumulator() {
|
||||
init_cpu()
|
||||
.test_line(": accum create 0 , does> dup >r @ + dup r> ! ;")
|
||||
.test_line("accum foo 1 foo 2 foo 3 foo")
|
||||
.expect_output("")
|
||||
.expect_stack([1, 3, 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_opcode_words() {
|
||||
// If it didn't recognize any of these then it would error
|
||||
init_cpu().test_line(": test + - / * % ^ & | not < > = @ ! c@ c! pop dup swap pick rot ;")
|
||||
.expect_output("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_novaforth_stats() {
|
||||
let heap: u32 = SYMBOLS["heap"].into();
|
||||
let heap_start: u32 = SYMBOLS["heap_start"].into();
|
||||
let data_start: u32 = SYMBOLS["data_start"].into();
|
||||
|
||||
println!("Bytes available: {}", 131072 - heap_start);
|
||||
println!("Text size: {}", data_start - 0x400);
|
||||
println!("Including dictionaries: {}", heap - 0x400);
|
||||
println!("Remaining in 4k: {}", 4096 - (heap - 0x400));
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use novaforth::ROM;
|
||||
use vcore::{Word, CPU};
|
||||
use vcore::memory::{PeekPoke, PeekPokeExt};
|
||||
use crate::constants::{SCREEN, SYMBOLS, TIB};
|
||||
use crate::memory_item::{MemoryItem, PointerTarget};
|
||||
|
||||
pub fn init_cpu() -> CPU {
|
||||
let mut cpu = CPU::new_random();
|
||||
for (i, b) in ROM.iter().enumerate() {
|
||||
cpu.poke(Word::from(0x400 + i), *b)
|
||||
}
|
||||
cpu
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub trait TestHarness {
|
||||
fn run_prelude(&mut self) -> &mut Self;
|
||||
fn test_fn(&mut self, name: &str) -> &mut Self;
|
||||
fn test_line(&mut self, line: &str) -> &mut Self;
|
||||
|
||||
fn given_stack<W: Into<Word>, I: IntoIterator<Item=W>>(&mut self, stack: I) -> &mut Self;
|
||||
fn given_memory<W: Into<Word>>(&mut self, addr: W, value: &str) -> &mut Self;
|
||||
|
||||
fn heap_bytes(&self, base: &str, offset: u32, len: u32) -> Vec<u8>;
|
||||
|
||||
fn expect_stack<W: Into<Word>, I: IntoIterator<Item=W>>(&self, stack: I) -> &Self;
|
||||
fn expect_empty_stack(&self) -> &Self;
|
||||
fn expect_rstack<W: Into<Word>, I: IntoIterator<Item=W>>(&self, stack: I) -> &Self;
|
||||
fn expect_empty_rstack(&self) -> &Self;
|
||||
fn expect_output(&self, output: &str) -> &Self;
|
||||
fn expect_memory<H: IntoIterator<Item=MemoryItem>>(&self, at: &str, items: H) -> u32;
|
||||
fn expect_heap<H: IntoIterator<Item=MemoryItem>>(&self, items: H) -> &Self;
|
||||
fn expect_pad<H: IntoIterator<Item=MemoryItem>>(&self, items: H) -> &Self;
|
||||
fn expect_4th_rstack<H: IntoIterator<Item=MemoryItem>>(&self, items: H) -> &Self;
|
||||
fn expect_pointer<T: Into<PointerTarget>>(&self, symbol: &str, target: T) -> &Self;
|
||||
fn expect_var(&self, symbol: &str, value: u32) -> &Self;
|
||||
fn expect_cursor(&self, offset: i32) -> &Self;
|
||||
}
|
||||
|
||||
impl TestHarness for CPU {
|
||||
fn run_prelude(&mut self) -> &mut Self {
|
||||
self.test_line(novaforth::PRELUDE)
|
||||
}
|
||||
|
||||
fn test_fn(&mut self, name: &str) -> &mut Self {
|
||||
self.push_call(SYMBOLS["stop"]);
|
||||
self.set_pc(SYMBOLS[name]);
|
||||
self.run_to_halt();
|
||||
self
|
||||
}
|
||||
|
||||
fn test_line(&mut self, line: &str) -> &mut Self {
|
||||
self.given_memory(TIB, line).given_stack([TIB]).test_fn("eval")
|
||||
}
|
||||
|
||||
fn given_stack<W: Into<Word>, I: IntoIterator<Item=W>>(&mut self, stack: I) -> &mut Self {
|
||||
for val in stack {
|
||||
self.push_data(val.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn given_memory<W: Into<Word>>(&mut self, addr: W, val: &str) -> &mut Self {
|
||||
let addr = addr.into();
|
||||
for (i, c) in val.chars().enumerate() {
|
||||
self.poke8(addr + i as u32, c as u8);
|
||||
}
|
||||
self.poke8(addr + val.len() as u32, 0u8);
|
||||
self
|
||||
}
|
||||
|
||||
fn heap_bytes(&self, base: &str, offset: u32, len: u32) -> Vec<u8> {
|
||||
let heap: u32 = SYMBOLS[base].into();
|
||||
let mut bytes = Vec::with_capacity(len as usize);
|
||||
for n in 0..len {
|
||||
bytes.push(self.peek8(n + heap + offset))
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
fn expect_stack<W: Into<Word>, I: IntoIterator<Item=W>>(&self, stack: I) -> &Self {
|
||||
let actual = self.get_stack();
|
||||
let expected = stack.into_iter().map(|w| w.into()).collect::<Vec<Word>>();
|
||||
assert_eq!(actual, expected);
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_empty_stack(&self) -> &Self {
|
||||
assert!(self.get_stack().is_empty());
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_rstack<W: Into<Word>, I: IntoIterator<Item=W>>(&self, stack: I) -> &Self {
|
||||
let actual = self.get_call();
|
||||
let expected = stack.into_iter().map(|w| w.into()).collect::<Vec<Word>>();
|
||||
assert_eq!(actual, expected);
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_empty_rstack(&self) -> &Self {
|
||||
assert!(self.get_call().is_empty());
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_output(&self, expected: &str) -> &Self {
|
||||
let len: u32 = self.peek24(SYMBOLS["emit_cursor"]).into();
|
||||
let mut actual = String::with_capacity(len as usize);
|
||||
for a in 0..len {
|
||||
actual.push(self.peek8(SCREEN + a) as char);
|
||||
}
|
||||
assert_eq!(expected, actual);
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_memory<H: IntoIterator<Item=MemoryItem>>(&self, at: &str, items: H) -> u32 {
|
||||
let mut delta = 0u32;
|
||||
|
||||
for item in items {
|
||||
if let Err(()) = item.check(self, at, Word::from(delta)) {
|
||||
let s = self.heap_bytes(at, delta, item.len()).into_iter().map(|b| format!("0x{:02X}", b)).collect::<Vec<_>>().join(", ");
|
||||
let exp_str = item.bytes(self).into_iter().map(|b| format!("0x{:02X}", b)).collect::<Vec<_>>().join(", ");
|
||||
panic!("Memory mismatch at {} + {}:\n\texpected {}\n\t\t{}\n\tactual\n\t\t{}", at, delta, item, exp_str, s)
|
||||
}
|
||||
delta += item.len();
|
||||
}
|
||||
|
||||
delta
|
||||
}
|
||||
|
||||
fn expect_heap<H: IntoIterator<Item=MemoryItem>>(&self, items: H) -> &Self {
|
||||
let delta = self.expect_memory("heap_start", items);
|
||||
assert_eq!(SYMBOLS["heap_start"] + delta, self.peek24(SYMBOLS["heap"]));
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_pad<H: IntoIterator<Item=MemoryItem>>(&self, items: H) -> &Self {
|
||||
self.expect_memory("pad", items);
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_4th_rstack<H: IntoIterator<Item=MemoryItem>>(&self, items: H) -> &Self {
|
||||
let delta = self.expect_memory("r_stack", items);
|
||||
assert_eq!(SYMBOLS["r_stack"] + delta, self.peek24(SYMBOLS["r_stack_ptr"]));
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_pointer<T: Into<PointerTarget>>(&self, symbol: &str, target: T) -> &Self {
|
||||
let actual = self.peek24(SYMBOLS[symbol]);
|
||||
let expected = target.into().addr(self);
|
||||
assert_eq!(expected, actual);
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_var(&self, symbol: &str, value: u32) -> &Self {
|
||||
let actual: u32 = self.peek24(SYMBOLS[symbol]).into();
|
||||
assert_eq!(value, actual);
|
||||
self
|
||||
}
|
||||
|
||||
fn expect_cursor(&self, offset: i32) -> &Self {
|
||||
let expected = (TIB as i32 + offset) as u32;
|
||||
let actual: u32 = self.peek24(SYMBOLS["cursor"]).into();
|
||||
assert_eq!(expected, actual);
|
||||
self
|
||||
}
|
||||
}
|
||||
Generated
+23
-6
@@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.5.0",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.26.3",
|
||||
"cm6-theme-gruvbox-dark": "^0.2.0",
|
||||
"codemirror": "^6.0.1",
|
||||
@@ -79,9 +80,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
|
||||
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.26.3",
|
||||
@@ -444,6 +448,11 @@
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@@ -686,9 +695,12 @@
|
||||
}
|
||||
},
|
||||
"@codemirror/state": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
|
||||
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"requires": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@codemirror/view": {
|
||||
"version": "6.26.3",
|
||||
@@ -853,6 +865,11 @@
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.5.0",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.26.3",
|
||||
"cm6-theme-gruvbox-dark": "^0.2.0",
|
||||
"codemirror": "^6.0.1",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {indentWithTab} from '@codemirror/commands'
|
||||
import {StreamLanguage} from '@codemirror/language'
|
||||
import {gruvboxDark} from 'cm6-theme-gruvbox-dark'
|
||||
import ForgeHighlighter from './forge_highlighter'
|
||||
import { Prec } from "@codemirror/state"
|
||||
|
||||
export default function ForgeEditor({ src, updateSrc, fileName }) {
|
||||
const editor = useRef(null)
|
||||
@@ -16,6 +17,12 @@ export default function ForgeEditor({ src, updateSrc, fileName }) {
|
||||
doc: src,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
// Normally F3 opens the search dialog, but we want to just eat it so the document's handler will get it.
|
||||
// We use F3 as a global shortcut for resetting the machine, even if the editor has focus.
|
||||
// Since the editor doesn't do anything for F1, 2, or 4, we don't need to do anything to ignore those.
|
||||
Prec.highest(
|
||||
keymap.of([{ key: "F3", run: () => { return true } }])
|
||||
),
|
||||
keymap.of([indentWithTab]),
|
||||
gruvboxDark,
|
||||
EditorView.updateListener.of((update) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default function({ src: defaultSrc }) {
|
||||
const [errors, setErrors] = useState(null) // What's displayed on the compile errors tab
|
||||
const [status, setStatus] = useState('') // The contents of the status bar
|
||||
const [stale, setStale] = useState(false) // Whether the editor has been changed since the last build
|
||||
const [keyFocus, setKeyFocus] = useState('editor') // The lens for interpreting key events: 'editor', 'emulator', 'filelist'
|
||||
// Whether the emulator should be running. Has to be a ref because the CPU setTimeout loop won't ever see changes in it otherwise
|
||||
const running = useRef(false)
|
||||
|
||||
@@ -131,6 +132,24 @@ export default function({ src: defaultSrc }) {
|
||||
content = <ForgeEditor build={build} run={run} updateSrc={updateSrc} src={src} fileName={currentFile}/>
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event) => {
|
||||
// F1-4 act the same in every mode:
|
||||
if (event.key === 'F3') {
|
||||
event.preventDefault()
|
||||
reset()
|
||||
}
|
||||
if (event.key === 'F4') {
|
||||
event.preventDefault()
|
||||
setKeyFocus('emulator')
|
||||
run()
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', listener)
|
||||
|
||||
return () => document.removeEventListener('keydown', listener)
|
||||
}, [reset, run])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabbar activeTab={activeTab} setActiveTab={setActiveTab} anyErrors={!!errors}/>
|
||||
@@ -168,7 +187,7 @@ function Tabbar({ activeTab, setActiveTab, anyErrors }) {
|
||||
const classNames = name => (name === activeTab ? 'button active' : 'button')
|
||||
return (
|
||||
<div className='tabbar'>
|
||||
<a className={classNames('editor')} onClick={onChangeTab} data-tab='editor'>[Editor]</a>
|
||||
<a className={classNames('editor')} onClick={onChangeTab} data-tab='editor'>[Editor (F2)]</a>
|
||||
<a className={classNames('assembly')} onClick={onChangeTab} data-tab='assembly'>[Assembly]</a>
|
||||
{anyErrors && <a className={classNames('errors')} onClick={onChangeTab} data-tab='errors'>[Errors]</a>}
|
||||
</div>
|
||||
@@ -176,25 +195,26 @@ function Tabbar({ activeTab, setActiveTab, anyErrors }) {
|
||||
}
|
||||
|
||||
// Toolbar of the controls for the simulator
|
||||
function Toolbar({activeTab, running, compile, build, run, stop, reset, addFile, removeFile }) {
|
||||
function Toolbar({ running, build, run, stop, reset, addFile, removeFile }) {
|
||||
let buildBtn, runBtn
|
||||
// TODO: Make this assemble on assembly tab, make that editable
|
||||
buildBtn = <a className='build' onClick={build}>[Build]</a>
|
||||
|
||||
if (running) {
|
||||
runBtn = <a className='stop' onClick={stop}>[Stop]</a>
|
||||
runBtn = <a className='stop' onClick={stop}>[Stop (F4)]</a>
|
||||
} else {
|
||||
runBtn = <a className='run' onClick={run}>[Run]</a>
|
||||
runBtn = <a className='run' onClick={run}>[Run (F4)]</a>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='file-buttons'>
|
||||
(F1)
|
||||
<a className='new' onClick={addFile}>[new]</a>
|
||||
<a className='del' onClick={removeFile}>[del]</a>
|
||||
</div>
|
||||
<div className='buttons'>
|
||||
<a className='reset' onClick={reset}>[Reset]</a>
|
||||
<a className='reset' onClick={reset}>[Reset (F3)]</a>
|
||||
{buildBtn}
|
||||
{runBtn}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user