From 676856dfe19334161dbd3cdf5025e382243be6e2 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Mon, 7 Sep 2026 14:49:29 -0500 Subject: [PATCH] translated n4th tests to rust --- Cargo.lock | 3 + vemu/src/main.rs | 6 +- vlua/src/lib.rs | 2 +- vtest/Cargo.toml | 5 +- vtest/n4th_test.lua | 904 ----------------------------------- vtest/src/constants.rs | 22 + vtest/src/lib.rs | 14 +- vtest/src/memory_item.rs | 166 +++++++ vtest/src/novaforth_tests.rs | 712 +++++++++++++++++++++++++++ vtest/src/test_harness.rs | 166 +++++++ 10 files changed, 1090 insertions(+), 910 deletions(-) delete mode 100644 vtest/n4th_test.lua create mode 100644 vtest/src/constants.rs create mode 100644 vtest/src/memory_item.rs create mode 100644 vtest/src/novaforth_tests.rs create mode 100644 vtest/src/test_harness.rs diff --git a/Cargo.lock b/Cargo.lock index cffce17..42f6be7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2143,6 +2143,9 @@ name = "vtest" version = "0.1.0" dependencies = [ "forge_core", + "lazy_static", + "novaforth", + "tinyjson", "vasm_core", "vcore", ] diff --git a/vemu/src/main.rs b/vemu/src/main.rs index 1b48305..e369d18 100644 --- a/vemu/src/main.rs +++ b/vemu/src/main.rs @@ -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(), diff --git a/vlua/src/lib.rs b/vlua/src/lib.rs index f9f6fd3..1524d4a 100644 --- a/vlua/src/lib.rs +++ b/vlua/src/lib.rs @@ -5,7 +5,7 @@ use mlua::{Error, UserData, UserDataMethods}; use vcore::memory::{PeekPoke, PeekPokeExt}; use std::iter::FromIterator; use tinyjson::JsonValue; -use novaforth::{ROM, PRELUDE, SYMBOLS}; +use novaforth::{ROM, SYMBOLS}; use vcore::opcodes::Opcode; #[mlua::lua_module] diff --git a/vtest/Cargo.toml b/vtest/Cargo.toml index c672784..bc54a48 100644 --- a/vtest/Cargo.toml +++ b/vtest/Cargo.toml @@ -8,4 +8,7 @@ edition = "2021" [dependencies] vcore = { path = "../vcore" } vasm_core = { path = "../vasm_core" } -forge_core = { path = "../forge_core" } \ No newline at end of file +forge_core = { path = "../forge_core" } +novaforth = { path = "../novaforth" } +lazy_static = "1.4.0" +tinyjson = "2.5.1" \ No newline at end of file diff --git a/vtest/n4th_test.lua b/vtest/n4th_test.lua deleted file mode 100644 index acb97b0..0000000 --- a/vtest/n4th_test.lua +++ /dev/null @@ -1,904 +0,0 @@ -package.cpath = package.cpath .. ';./target/release/?.dylib' -Vlua = require('libvlua') - -TIB = 80000 -- Just a convenient place to stick a terminal input buffer for tests. Could be any number. - -function init_cpu() - local cpu = Vlua.new() - cpu:load_rom() - --cpu:init_serial(2) - --cpu:poke24(Vlua.symbol('emit_hook'), Vlua.symbol('test_emit')) - - return cpu -end - -function call(cpu, symbol) - cpu:push_call(Vlua.symbol('stop')) - cpu:set_pc(Vlua.symbol(symbol)) - cpu:run() -end - -function test_fn(name, setup, check) - local cpu = init_cpu() - setup(cpu) - call(cpu, name) - local st = cpu:stack() - local rst = cpu:r_stack() - check(st, get_output(cpu), cpu, rst) -end - -function get_output(cpu) - local start = 0x10000 - local len = cpu:peek24(Vlua.symbol('emit_cursor')) - local str = '' - for a = start, len+start-1 do - str = str .. string.char(cpu:peek(a)) - end - return str -end - -function array_eq(a1, a2) - local eq = true - for i, n in ipairs(a1) do - if n ~= a2[i] then eq = false end - end - if #a1 ~= #a2 then eq = false end - - if eq then return true end - - local lt = '' - for i, n in ipairs(a1) do - lt = lt .. string.format('0x%x ', n) - end - - local rt = '' - for i, n in ipairs(a2) do - rt = rt .. string.format('0x%x ', n) - end - - print(string.format('Arrays not equal!\nlt: { %s }\nrt: { %s }', lt, rt)) - return false -end - -function given_stack(contents) - return function(cpu) for _, n in ipairs(contents) do cpu:push_data(n) end end -end - -function given_memory(at, contents) - if type(contents) == 'string' then - contents = { contents:byte(1, #contents) } - table.insert(contents, 0) - elseif type(contents) == 'number' then - contents = { contents } - end - return function(cpu) - for i, b in ipairs(contents) do cpu:poke(at + i - 1, b) end - end -end - -function given_word(at, word) - return function(cpu) cpu:poke24(at, word) end -end - -function expect_stack(expected) - return function(actual) assert(array_eq(expected, actual)) end -end - -function expect_r_stack(expected) - return function(_st, _out, _cpu, actual) assert(array_eq(expected, actual)) end -end - -function expect_output(expected) - return function(_s, actual) assert(expected == actual, string.format('exp %q, act %q', expected, actual)) end -end - -function expect_memory(start, ...) - local mem = { ... } - local expanded_mem = {} - for _, el in ipairs(mem) do - if type(el) == 'string' then table.insert(expanded_mem, el:byte()) - elseif type(el) == 'table' then - for _, b in ipairs(el) do table.insert(expanded_mem, b) end - else - table.insert(expanded_mem, el) - end - end - - return function(_s, _o, cpu) - for i, b in ipairs(expanded_mem) do - local actual = cpu:peek(start + i - 1) - assert(actual == b, string.format('0x%x: exp %d, act %d', start + i - 1, b, actual)) - end - end -end - -function word(val) - val = val & 0xffffff - return { val & 0xff, (val & 0xff00) / 256, (val & 0xff0000) / 65536 } -end - -function op(mnemonic, args) - if not args then args = 0 end - return Vlua.opcode_for(mnemonic) * 4 + args -end - -function inst(mnemonic, arg) - local o = op(mnemonic, 3) - local w = word(arg) - return { o, w[1], w[2], w[3] } -end - -function call_inst(symbol) - return inst('call', Symbols[symbol]) -end - -function expect_string(start, str) - return function(_s, _o, cpu) - for i = 1, #str do - local actual = cpu:peek(start + i - 1) - assert(actual == str:byte(i), string.format('%x: exp %q, act %q (%d)', start + i - 1, str:sub(i,i), string.char(actual), actual)) - end - assert(cpu:peek(start + #str - 1), string.format('%x exp 0, act %d', start + #str - 1, cpu:peek(start + #str - 1))) - end -end - -function expect_word(addr, val) - return function(_s, _o, cpu) - local actual = cpu:peek24(addr) - assert(actual == val, string.format('exp %xh, act %xh', val, actual)) - end -end - -function expect_heap_advance(n) - return function(_s, _o, cpu) - local expected = heap(0) + n - local actual = cpu:peek24(Vlua.symbol('heap')) - assert(actual == expected, string.format('heap should advance %d, actual %d', n, actual - heap(0))) - end -end - -function expect_cursor(n) - return function(_s, _o, cpu) - local expected = TIB + n - local actual = cpu:peek24(Vlua.symbol('cursor')) - assert(actual == expected, string.format('cursor should advance %d, actual %d', n, actual - TIB)) - end -end - -function expect_4th_rstack(stack) - local words = {} - for _, v in ipairs(stack) do table.insert(words, word(v)) end - return all( - expect_memory(Vlua.symbol('r_stack'), table.unpack(words)), - expect_word(Vlua.symbol('r_stack_ptr'), Vlua.symbol('r_stack') + #stack * 3)) -end - -function dump_memory(addr, len) - return function(_s, _o, cpu) - for a = addr, addr + len do - local b = cpu:peek(a) - local c = string.char(b) - local op = Opcodes.mnemonic_for(math.floor(b / 4)) - local args = b & 3 - if b < 32 then c = '' end - print(string.format('[%d]\t%xh:\t%xh\t(%d)\t%q\t%q/%d', a - addr, a, b, b, c, op, args)) - end - end -end - -function all(...) - local fns = { ... } - return function(...) - for i, f in ipairs(fns) do f(...) end - end -end - -function test_line(line, ...) - test_fn('eval', all(given_stack{TIB}, given_memory(TIB, line)), all(...)) -end - -function test_lines(lines, ...) - local cpu = init_cpu() - - for _, line in ipairs(lines) do - cpu:push_data(TIB) - local contents = { line:byte(1, #line) } - table.insert(contents, 0) - for i, b in ipairs(contents) do cpu:poke(TIB + i - 1, b) end - call(cpu, 'eval') - end - - local st = cpu:stack() - local rst = cpu:r_stack() - local check = all(...) - check(st, get_output(cpu), cpu, rst) -end - -PRELUDE = 34 -- How many bytes the prelude adds to the heap -function test_prelude_line(line, ...) - -- local prelude1 = ": cont ' $ jmp #asm ; immediate" - local prelude1 = 'create :: ] create continue ] [' - local prelude2 = ':: ;; postpone exit continue [ [ immediate' - test_lines({ prelude1, prelude2, line }, ...) -end - -function heap(offset) - return Vlua.symbol('heap_start') + offset -end - -function dump_symbols() - reverse = {} - addrs = {} - for sym, addr in pairs(Symbols) do reverse[addr] = sym; table.insert(addrs, addr) end - table.sort(addrs) - for _, addr in ipairs(addrs) do - print(string.format('0x%x\t%s', addr, reverse[addr])) - end -end - --------------------------------------------------- - -test_fn('dupnz', - given_stack{ 3 }, - expect_stack{ 3, 3 }) - -test_fn('dupnz', - given_stack{ 0 }, - expect_stack{ 0 }) - --------------------------------------------------- - -test_line('10 ?dup', expect_stack{10, 10}) -test_line('0 ?dup', expect_stack{0}) - --------------------------------------------------- - -test_line('10', expect_stack{10}) -test_line('10 20 30', - expect_stack{10, 20, 30}, - expect_r_stack{}) - --------------------------------------------------- - --- Evaluating gibberish -test_line('notaword', expect_output('Not a word: notaword\n')) - --------------------------------------------------- - -test_line('create blah', - expect_word(Vlua.symbol('heap'), heap(11)), -- Heap ptr is advanced by the entry length - expect_memory(heap(0), 'blah\0'), -- New dict entry has the name - expect_word(heap(5), heap(11)), -- Followed by the new heap ptr - expect_word(heap(8), Vlua.symbol('dict_start')), -- Next ptr is the old dict head - expect_word(Vlua.symbol('dictionary'), heap(0))) -- Dict has had the new entry consed on to it - --------------------------------------------------- - --- Exiting and entering immediate mode -test_line(']', expect_word(Vlua.symbol('handleword_hook'), Vlua.symbol('compile_handleword'))) -test_line('] [', all( - expect_word(Vlua.symbol('handleword_hook'), Vlua.symbol('immediate_handleword')), - expect_r_stack{})) - --------------------------------------------------- - --- Compiling a number -test_line('] 122773', - expect_word(Vlua.symbol('heap'), heap(4)), -- Advance heap by the length of an instruction - expect_memory(heap(0), { 3, 149, 223, 1})) -- A push instruction for 122773 - --- Compiling a call to a word -test_line('] create', - expect_word(Vlua.symbol('heap'), heap(4)), -- Advance heap by the length of an instruction - expect_memory(heap(0), { Vlua.opcode_for('call') * 4 + 3 }), -- A call instruction - expect_word(heap(1), Vlua.symbol('nova_create'))) -- ...to nova_create - --- Compiling gibberish -test_line('] stillnotaword', expect_output('Not a word: stillnotaword\n')) - --------------------------------------------------- - --- -- Continue word (compiles a jmp) -test_line('] continue ]', - expect_word(Vlua.symbol('heap'), heap(4)), -- Advance heap by the length of an instruction - expect_memory(heap(0), { Vlua.opcode_for('jmp') * 4 + 3 }), -- A call instruction - expect_word(heap(1), Vlua.symbol('nova_close_bracket'))) -- ...to nova_close_bracket - --- Continue compile word -test_line('] continue [', - expect_word(Vlua.symbol('heap'), heap(4)), -- Advance heap by the length of an instruction - expect_memory(heap(0), { Vlua.opcode_for('jmp') * 4 + 3 }), -- A call instruction - expect_word(heap(1), Vlua.symbol('nova_open_bracket'))) -- ...to nova_close_bracket - --- Continue gibberish -test_line('] continue supernotword', expect_output('Not a word: supernotword\n')) - --- Implement continue with #asm! -test_lines({ ": cont ' $ jmp #asm ; immediate", - '] cont ]' }, - expect_memory(heap(11), -- Just skip cont's header - inst('call', Vlua.symbol('nova_tick')), -- Call tick to see what we're continuing to - inst('push', Vlua.opcode_for('jmp')), -- Push a jmp - inst('call', Vlua.symbol('compile_instruction_arg')), -- Compile a jmp to that word - op('ret'), -- Return from cont - inst('jmp', Vlua.symbol('nova_close_bracket')))) -- Cont gives us a jmp to `]` - --- Prelude continue with a runtime word -test_lines({ ": cont ' $ jmp #asm ; immediate", - '] cont print' }, - expect_memory(heap(11 + 13), -- Just skip cont's header and impl - inst('jmp', Vlua.symbol('print')))) -- Cont gives us a jmp to `print` - --------------------------------------------------- - --- Prelude colon definition -test_lines({ ": cont ' $ jmp #asm ; immediate", - 'create :: ] create cont ] [' }, - expect_memory(heap(24), '::\0'), -- New dict entry has the name (24 bytes for cont) - expect_word(heap(24 + 3), heap(24 + 9)), -- Followed by the ptr to the fn - expect_memory(heap(24 + 9), inst('call', Vlua.symbol('nova_create'))), -- Which is a call to create... - expect_memory(heap(24 + 13), inst('jmp', Vlua.symbol('nova_close_bracket'))), -- Followed by jmping to close_bracket - expect_word(Vlua.symbol('handleword_hook'), Vlua.symbol('immediate_handleword')), -- And now we're back in immediate mode - expect_r_stack{}) -- And haven't leaked a stack frame - --- Using prelude colon -test_lines({ "create cont ] ' $ jmp #asm ; immediate", - 'create :: ] create cont ] [ :: foo 35' }, - expect_memory(heap(24 + 17), 'foo\0'), -- A new entry for foo - expect_word(heap(24 + 21), heap(24 + 27)), -- Defn ptr is the new heap - expect_memory(heap(24 + 27), inst('push', 35)), -- fn begins with pushing a 35 - expect_word(Vlua.symbol('handleword_hook'), Vlua.symbol('compile_handleword')), -- We're still in compile mode - expect_r_stack{}) -- And haven't leaked a stack frame - --------------------------------------------------- - --- Postponing normal words -test_line('] postpone create', - expect_memory(heap(0), inst('push', Vlua.symbol('nova_create'))), - expect_memory(heap(4), inst('push', Vlua.opcode_for('call'))), - expect_memory(heap(8), inst('call', Vlua.symbol('compile_instruction_arg')))) - --- Postponing compile words -test_line('] postpone [', expect_memory(heap(0), inst('call', Vlua.symbol('nova_open_bracket')))) - --- Postponing gibberish -test_line('] postpone reallynotaword', expect_output('Not a word: reallynotaword\n')) - --------------------------------------------------- - --- Compile a ret -test_line('] exit', expect_memory(heap(0), { op('ret') })) - --------------------------------------------------- ---- Prelude stuff: ------------------------------- --------------------------------------------------- - --- 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. - --- Prelude semicolon definition -test_line('create :: ] create continue ] [ :: ;; postpone exit continue [ [ immediate', - expect_memory(heap(17), ';', ';', 0), -- A new entry for semicolon - expect_memory(heap(26), - inst('call', Vlua.symbol('nova_exit')), -- Which compiles a ret - inst('jmp', Vlua.symbol('nova_open_bracket'))), -- And then returns to immediate mode - expect_word(Vlua.symbol('compile_dictionary'), heap(17)), -- Semicolon is in the compile dict - expect_word(heap(23), Vlua.symbol('compile_dict_start')), -- Semicolon points at old compile_dict head - expect_word(Vlua.symbol('handleword_hook'), Vlua.symbol('immediate_handleword'))) -- In immediate mode again - --- Using prelude semicolon -test_prelude_line('] ;;', - expect_memory(heap(PRELUDE), op('ret')), -- Compiled our ret - expect_word(Vlua.symbol('handleword_hook'), Vlua.symbol('immediate_handleword')), -- In immediate mode again - expect_r_stack{}) -- And haven't leaked a stack frame - --- Defining a word and calling it, with the prelude -test_prelude_line(':: fives 5 5 5 ;; fives', - expect_stack{ 5, 5, 5 }, - expect_r_stack{}) - --- Testing create / does> without compile-time behavior, with the prelude -test_prelude_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{ heap(PRELUDE + 11 + 22 + 12), 2, 3 }, -- - -- Body of blah: - expect_memory(heap(PRELUDE + 11), - inst('call', Vlua.symbol('nova_create')), -- After blah's header, we have a call to create - inst('push', heap(PRELUDE + 11 + 13)), -- push the address of after the does> - inst('jmp', Vlua.symbol('does_at_runtime')), -- And a call to does@runtime, to start compiling it - op('ret'), -- blah's return - inst('push', 2), -- The runtime behavior of fnord (the "mold"): - inst('push', 3), - op('ret')), -- fnord's runtime return - -- Header of fnord: - expect_memory(heap(PRELUDE + 11 + 22), - 'f', 'n', 'o', 'r', 'd', 0, -- the new word's header - word(heap(PRELUDE + 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(PRELUDE), right after the prelude: - word(heap(PRELUDE))), - -- Body (trampoline) of fnord: - expect_memory(heap(PRELUDE + 11 + 22 + 12), - inst('push', heap(PRELUDE + 11 + 22 + 12)), -- Push the old value, which was right - -- after the header (because of the null compile-time behavior) - inst('jmp', heap(PRELUDE + 11 + 13)))) -- jmp to the runtime behavior, after the does> call - --- Testing create / does> when there's compile-time behavior, with the prelude -test_prelude_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, PRELUDE + 11 (blah's entry) + 26 (blah's body, part of which is fnord's) + 12 (fnord's entry) - expect_stack{ heap(PRELUDE + 11 + 26 + 12), 3 }, - -- Body of blah: - expect_memory(heap(PRELUDE + 11), - inst('call', Vlua.symbol('nova_create')), -- After blah's header, we have a call to create - inst('push', 15), - inst('call', Vlua.symbol('nova_comma')), - inst('push', heap(PRELUDE + 11 + 21)), -- push the address of after the does> - inst('jmp', Vlua.symbol('does_at_runtime')), -- And a call to does@runtime, to start compiling it - op('ret'), -- blah's return - inst('push', 3), -- After the does>; the runtime behavior of fnord (the "mold"): - op('ret')), -- fnord's runtime return - -- Header of fnord: - expect_memory(heap(PRELUDE + 11 + 26), - 'f', 'n', 'o', 'r', 'd', 0, -- the new word's header - word(heap(PRELUDE + 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 right after the prelude at heap(PRELUDE): - word(heap(PRELUDE))), - expect_memory(heap(PRELUDE + 11 + 26 + 12), - word(15)), -- The compile time behavior compiled this 15 - -- Body (trampoline) of fnord: - expect_memory(heap(PRELUDE + 11 + 26 + 15), - inst('push', heap(PRELUDE + 11 + 26 + 12)), -- Push the old value, which was right - -- after the header, the 15 we compiled - inst('jmp', heap(PRELUDE + 11 + 21)))) -- jmp to the runtime behavior, after the does> call - --------------------------------------------------- - --- Defining a word and calling it, with the normal colon / semicolon words -test_line(': fives 5 5 5 ; fives', - expect_stack{ 5, 5, 5 }, - expect_r_stack{}) - --------------------------------------------------- - --- Basic use of asm -test_line('create execute $ jmp asm', - expect_heap_advance(15), - expect_word(heap(8), heap(14)), - expect_memory(heap(14), op('jmp'))) - --- Asm with args -test_line('45 $ push #asm', - expect_memory(heap(0), inst('push', 45)), - expect_word(Vlua.symbol('heap'), heap(4))) - --------------------------------------------------- - --- Compile-mode asm -test_line('] $ jmp asm', - expect_heap_advance(8), - expect_memory(heap(0), - inst('push', Vlua.opcode_for('jmp')), - inst('call', Vlua.symbol('compile_instruction')))) - --- Compile-mode asm with args -test_line('] 45 $ xor #asm', - expect_heap_advance(12), - expect_memory(heap(0), - inst('push', 45), - inst('push', Vlua.opcode_for('xor'), - inst('call', Vlua.symbol('compile_instruction_arg'))))) - -test_line(': foo 34 $ xor #asm ; immediate ] foo', - expect_memory(heap(0), - -- foo's header - 'f', 'o', 'o', 0, word(heap(10)), word(Vlua.symbol('compile_dict_start')), - inst('push', 34), -- Push an arg - inst('push', Vlua.opcode_for('xor')), -- Push an opcode - inst('call', Vlua.symbol('compile_instruction_arg')), -- Compile that with an arg - op('ret'), -- Return from foo - -- Foo is now an immediate word, and when we call it in compile mode... - inst('xor', 34))) -- It compiles a xor 34 - -test_line('$ xor 3', expect_stack{9, 3}) -test_line('$ blah 3', expect_stack{}, expect_output('Invalid mnemonic: blah\n')) - -test_line('] $ xor 3', - expect_stack{}, - expect_memory(heap(0), - inst('push', 9), - inst('push', 3)), - expect_heap_advance(8)) - -test_line('] $ blah 3', - expect_stack{}, - expect_output('Invalid mnemonic: blah\n'), - expect_heap_advance(0)) -- It hits quit right after the error - --------------------------------------------------- - --- Comma compile a number -test_line('1234 ,', - expect_word(heap(0), 1234), - expect_heap_advance(3)) - --------------------------------------------------- - --- Tick a word -test_line("' print", expect_stack{ Vlua.symbol('print') }) - --- Bracket-tick a word -test_line("] ['] print", - expect_memory(heap(0), inst('push', Vlua.symbol('print'))), - expect_heap_advance(4)) - --- Tick gibberish -test_line("' bananas", - expect_stack{}, - expect_r_stack{}, - expect_output('Not a word: bananas\n')) - --- Bracket-tick gibberish -test_line("] ['] penguin", - expect_stack{}, - expect_r_stack{}, - expect_output('Not a word: penguin\n')) - --- Tick a compile word -test_line("' [", expect_stack{ Vlua.symbol('nova_open_bracket') }) - --- Bracket-tick a compile word -test_line("] ['] does>", - expect_memory(heap(0), inst('push', Vlua.symbol('does_word'))), - expect_heap_advance(4)) - --------------------------------------------------- - --- Fetch the pad address -test_line(' pad ', expect_stack{ Vlua.symbol('pad') }) - --- Read a word to the pad -test_line('word mango', - expect_output(''), - expect_stack{ Vlua.symbol('pad') }, - expect_memory(Vlua.symbol('pad'), 'm', 'a', 'n', 'g', 'o', 0)) - --------------------------------------------------- - --- Literal, compiles a push instruction -test_line('1234 ] literal', - expect_stack{}, - expect_memory(heap(0), inst('push', 1234)), - expect_heap_advance(4)) - --------------------------------------------------- - --- Paren comments -test_line('1 2 ( 3 4 5 ) 6', expect_stack{1, 2, 6}) - --- Nested paren comments -test_line('1 2 ( ( 3 4 ) 5 6', expect_stack{1, 2}) - --- Compiled paren comments -test_line('] 1 2 ( 3 4 5 ) 6', expect_heap_advance(12)) - --- Compiled nested paren comments -test_line('] 1 2 ( ( 3 4 ) 5 6', expect_heap_advance(8)) - --- Backslash comments -test_lines({ '1 2 \\ 3 4', '5 6' }, expect_stack{1, 2, 5, 6}) - --- Compiled backslash comments -test_lines({ '] 1 2 \\ 3 4', '5 6' }, expect_heap_advance(16)) - --------------------------------------------------- - --- Parse numbers from words -test_line('number 17', expect_stack{ 17, 1 }) -test_line('number blah', expect_stack{ 0 }) -test_line('number -23', expect_stack{ (-23 & 0xffffff), 1 }) - --- Parse hex numbers from words -test_line('hex number a4', expect_stack{ 164, 1 }) -test_line('hex number blah', expect_stack{ 0 }) - --- Switch between hex and dec -test_line('hex number a4 dec number 23', expect_stack{ 164, 1, 23, 1 }) -test_line('hex a4 dec 23', expect_stack{ 164, 23 }) - --------------------------------------------------- - --- Output in hex and dec -test_line('hex a4 . dec 23 .', expect_output('a423')) -- Yeah, no separator -test_line('hex a4 dec .', expect_output('164')) -test_line('dec 525 hex .', expect_output('20d')) -test_line('-15 .', expect_output('-15')) - --------------------------------------------------- - --- Compiling strings to the heap -test_line('s" foo"', - expect_stack{heap(0)}, - expect_cursor(7), - expect_memory(heap(0), 'f', 'o', 'o', 0), - expect_heap_advance(4)) - --- Compiling empty string -test_line('s" "', - expect_stack{heap(0)}, - expect_memory(heap(0), 0), - expect_heap_advance(1)) - --- Unterminated string -test_line('s" foo', - expect_stack{}, - expect_heap_advance(0), - expect_cursor(6), - expect_output('Unclosed string')) - --- Compile move squote -test_line('] s" blah"', - expect_memory(heap(0), - inst('jmpr', 9), -- length of the jmpr itself + 'blah\0' - 'b', 'l', 'a', 'h', 0, -- The actual string - inst('push', heap(4))), -- Push the addr of the string - expect_heap_advance(13)) - --- Compile mode unterminated string -test_line('] s" foo', - expect_heap_advance(0), - expect_output('Unclosed string')) - --------------------------------------------------- - --- Basic output -test_line('." foo"', - expect_stack{}, expect_heap_advance(0), - expect_output('foo')) - --- Compile output -test_line('] ." foo"', - expect_heap_advance(16), - expect_memory(heap(0), - inst('jmpr', 8), - 'f', 'o', 'o', 0, - inst('push', heap(4)), - inst('call', Vlua.symbol('print')))) - --- Unterminated output -test_line('." foo', - expect_stack{}, expect_heap_advance(0), - expect_output('Unclosed string')) - --- Compile output -test_line('] ." foo', - expect_heap_advance(0), - expect_output('Unclosed string')) - --------------------------------------------------- - --- Test print fn -test_line('s" foo" print', - expect_cursor(13), - expect_heap_advance(4), - expect_output('foo')) - --------------------------------------------------- - --- Test compare -test_line('s" foo" s" bar" compare', expect_stack{0}) -test_line('s" foo" s" foo" compare', expect_stack{1}) -test_line('s" foo" ?dup compare', expect_stack{1}) -- There's no simple dup... -test_line('s" foo" s" foo234" compare', expect_stack{0}) -test_line('s" foo123" s" foo" compare', expect_stack{0}) - --------------------------------------------------- - --- Print the stack -test_line('10 20 30 .s', - expect_stack{ 10, 20, 30 }, - expect_output('<< 10 20 30 >>')) - --- Print the stack in hex -test_line('10 20 30 hex .s', - expect_stack{ 10, 20, 30 }, - expect_output('<< a 14 1e >>')) - --- Print nothing -test_line('.s', - expect_stack{}, - expect_output('<< >>')) - --------------------------------------------------- - --- pushr, peekr -test_line('3 >r r@', - expect_stack{3}, - expect_4th_rstack{3}) - --- popr -test_line('3 >r 5 r>', - expect_stack{5, 3}, - expect_4th_rstack{}) - --- rpick -test_line('10 20 30 >r >r >r 2 rpick', - expect_stack{30}, - expect_4th_rstack{30, 20, 10}) - --------------------------------------------------- - --- Heap ptr stuff -test_line('&heap', expect_stack{Vlua.symbol('heap')}) -test_line('here', expect_stack{heap(0)}) - --------------------------------------------------- - --- To-asm -test_line('$ brnz >asm', - expect_stack{}, - expect_heap_advance(4), - expect_memory(heap(0), inst('brnz', 0)), - expect_4th_rstack{heap(1)}) - --- Resolve -test_line('$ brnz >asm resolve', - expect_heap_advance(4), - expect_4th_rstack{}, - expect_memory(heap(0), - inst('brnz', 4))) -- brnz 12 ahead - --------------------------------------------------- - --- An 'if' implementation -test_line(': if $ brz >asm ; immediate ] if', - expect_stack{}, - expect_heap_advance(9 + 9 + 4), -- Entry 'if', body of 'if' (push, call, ret), and the brnz we just compiled - expect_4th_rstack{heap(9 + 9 + 1)}, -- Address of said brnz' arg - expect_memory(heap(9), -- Skipping if's entry - inst('push', Vlua.opcode_for('brz')), inst('call', Vlua.symbol('nova_asm_to')), op('ret'), -- if's body - inst('brz', 0))) -- The unresolved brnz 'if' compiled - --- If / then -test_lines({ ': if $ brz >asm ; immediate', - ': then resolve ; immediate', - ': foo if 2 then ;', - '1 foo 10 0 foo' }, - expect_stack{2, 10}) - --- If / else / then -test_lines({ ': if $ brz >asm ; immediate', - ': then resolve ; immediate', - ': else r> $ jmpr >asm >r resolve ; immediate', - ': foo if 2 else 3 then ;', - '1 foo 10 0 foo' }, - expect_stack{2, 10, 3}) - --------------------------------------------------- - --- Begin / until loops -test_lines({ ': 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 - ': until r> here - $ brz #asm ; immediate', - -- This ought to loop from 5..0, leaving each one on the stack - ': foo 5 begin dup 1 - dup not until ; foo' }, - expect_stack{5, 4, 3, 2, 1, 0}) - --- do / loop counted loops -test_lines({ 'create 1+ 1 $ add #asm ] ;', - ': do postpone swap postpone >r postpone >r here >r ; immediate', - ': _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 - ': unloop r> r> pop pop ;', - ': loop postpone _loop_test r> here - $ brnz #asm postpone unloop ; immediate', - ': foo 3 0 do 33 loop ; foo' }, - expect_stack{33, 33, 33}) - --------------------------------------------------- - --- Testing quit as called by an error -test_line('2 3 : foo nooope ; 7', - expect_heap_advance(10), -- It does the header but that's it - expect_output('Not a word: nooope\n'), -- Spits out an error message - expect_word(Vlua.symbol('handleword_hook'), Vlua.symbol('immediate_handleword')), -- Back in immediate mode - expect_stack{}) -- Clobbers the stack - --- Testing quit as called manually -test_lines({ ': low 3 quit 65 emit ;', - ': med 2 low 66 emit ;', - ': high 1 med 67 emit ;', - 'high' }, - expect_output(''), -- This isn't an error, we just quit - expect_stack{}) -- We quit partway through 'low', so skip all the frames above that - --------------------------------------------------- - --- Testing immediate-mode lambdas -test_line('{ 3 5 }', - expect_heap_advance(0), -- It does not move the heap - expect_output(''), - expect_word(Vlua.symbol('handleword_hook'), Vlua.symbol('immediate_handleword')), -- Back in immediate mode - expect_stack{Vlua.symbol('heap_start')}, -- Leaves the address of the lambda on the stack - expect_memory(heap(0), - inst('push', 3), - inst('push', 5), - op('ret'))) - -test_line('{ 3 5 } execute', - expect_stack{ 3, 5 }) -- Runs the anonymous fn - --- Compile-mode lambda, non-nested -test_line(': foo 1 { 2 } ; foo', - expect_output(''), - expect_heap_advance(10 + 4 + 4 + 4 + 1 + 4 + 1), -- header, push, jmpr, push, ret, push, ret - expect_memory(heap(10), - inst('push', 1), - inst('jmpr', 4 + 4 + 1), -- jmpr, push, ret - inst('push', 2), - op('ret'), - inst('push', heap(10 + 4 + 4)), -- header, push(1), jmpr - op('ret')), - expect_stack{ 1, heap(10 + 4 + 4) }) - --- Compile-mode lambda, nested -test_line(': foo 1 { 2 { 3 } } ; foo', - expect_output(''), - expect_heap_advance(10 + 4 + 4 + 4 + 4 + 4 + 1 + 4 + 1 + 4 + 1), -- header, push, jmpr, push, ret, push, ret - expect_memory(heap(10), - inst('push', 1), - inst('jmpr', 4 + 4 + 4 + 4 + 1 + 4 + 1), -- jmpr, push(2), jmpr, push(3), ret, push(inner-lambda), ret - inst('push', 2), - inst('jmpr', 4 + 4 + 1), -- inner lambda: jmpr, push, ret - inst('push', 3), - op('ret'), - inst('push', heap(10 + 4 + 4 + 4 + 4)), -- push the inner-lambda addr - op('ret'), - inst('push', heap(10 + 4 + 4)), - op('ret') - ), - expect_stack{ 1, heap(10 + 4 + 4) }, - expect_word(Vlua.symbol('lambda_nesting_level'), 0)) - --- Executing nested compile-mode lambdas -test_line(': foo 1 { 2 { 3 } } ; foo execute execute', - expect_stack{ 1, 2, 3 }) - --------------------------------------------------- - --- A Graham accumulator -test_lines({ ': accum create 0 , does> dup >r @ + dup r> ! ;', - 'accum foo 1 foo 2 foo 3 foo' }, - expect_output(''), - expect_stack{ 1, 3, 6 }) - --------------------------------------------------- - --- Test that single-opcode words exist, at least: -test_line(': test + - / * % ^ & | not < > = @ ! c@ c! pop dup swap pick rot ;', - expect_output('')) -- If it didn't recognize any of these then it would error - --------------------------------------------------- - ---[==[ - TODOs - - `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 TODOs - - Remove 'continue', we can implement it ourselves easily - - Prelude of simple words - - Rewrite / macro-ize string fns ---]==] - --------------------------------------------------- - -print('Bytes available: ' .. 131072 - heap(0)) -print('Text size: ' .. Vlua.symbol('data_start') - 0x400) -print('Including dictionaries: ' .. Vlua.symbol('heap') - 0x400) -print('Remaining in 4k: ' .. 4096 - (Vlua.symbol('heap') - 0x400)) diff --git a/vtest/src/constants.rs b/vtest/src/constants.rs new file mode 100644 index 0000000..3039752 --- /dev/null +++ b/vtest/src/constants.rs @@ -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 = { + 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; \ No newline at end of file diff --git a/vtest/src/lib.rs b/vtest/src/lib.rs index d00509c..1fe51a3 100644 --- a/vtest/src/lib.rs +++ b/vtest/src/lib.rs @@ -2,4 +2,16 @@ mod integration_tests; #[cfg(test)] -mod forge_tests; \ No newline at end of file +mod forge_tests; + +#[cfg(test)] +mod novaforth_tests; + +#[cfg(test)] +mod memory_item; + +#[cfg(test)] +mod constants; + +#[cfg(test)] +mod test_harness; \ No newline at end of file diff --git a/vtest/src/memory_item.rs b/vtest/src/memory_item.rs new file mode 100644 index 0000000..a99b057 --- /dev/null +++ b/vtest/src/memory_item.rs @@ -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>), + /// 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>(&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 { + 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, "", h, SYMBOLS["heap_start"] + *h), + PointerTarget::NewHeap => write!(f, ""), + } + } +} + +impl From 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>(val: P) -> MemoryItem { MemoryItem::Pointer(val.into()) } +pub fn heap>(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>(val: W) -> MemoryItem { MemoryItem::Value(val.into()) } +pub fn inst4>(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 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"]), + } + } +} \ No newline at end of file diff --git a/vtest/src/novaforth_tests.rs b/vtest/src/novaforth_tests.rs new file mode 100644 index 0000000..9770dc0 --- /dev/null +++ b/vtest/src/novaforth_tests.rs @@ -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)); +} \ No newline at end of file diff --git a/vtest/src/test_harness.rs b/vtest/src/test_harness.rs new file mode 100644 index 0000000..1689ff6 --- /dev/null +++ b/vtest/src/test_harness.rs @@ -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, I: IntoIterator>(&mut self, stack: I) -> &mut Self; + fn given_memory>(&mut self, addr: W, value: &str) -> &mut Self; + + fn heap_bytes(&self, base: &str, offset: u32, len: u32) -> Vec; + + fn expect_stack, I: IntoIterator>(&self, stack: I) -> &Self; + fn expect_empty_stack(&self) -> &Self; + fn expect_rstack, I: IntoIterator>(&self, stack: I) -> &Self; + fn expect_empty_rstack(&self) -> &Self; + fn expect_output(&self, output: &str) -> &Self; + fn expect_memory>(&self, at: &str, items: H) -> u32; + fn expect_heap>(&self, items: H) -> &Self; + fn expect_pad>(&self, items: H) -> &Self; + fn expect_4th_rstack>(&self, items: H) -> &Self; + fn expect_pointer>(&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, I: IntoIterator>(&mut self, stack: I) -> &mut Self { + for val in stack { + self.push_data(val.into()); + } + self + } + + fn given_memory>(&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 { + 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, I: IntoIterator>(&self, stack: I) -> &Self { + let actual = self.get_stack(); + let expected = stack.into_iter().map(|w| w.into()).collect::>(); + assert_eq!(actual, expected); + self + } + + fn expect_empty_stack(&self) -> &Self { + assert!(self.get_stack().is_empty()); + self + } + + fn expect_rstack, I: IntoIterator>(&self, stack: I) -> &Self { + let actual = self.get_call(); + let expected = stack.into_iter().map(|w| w.into()).collect::>(); + 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>(&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::>().join(", "); + let exp_str = item.bytes(self).into_iter().map(|b| format!("0x{:02X}", b)).collect::>().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>(&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>(&self, items: H) -> &Self { + self.expect_memory("pad", items); + self + } + + fn expect_4th_rstack>(&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>(&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 + } +} \ No newline at end of file