translated n4th tests to rust

This commit is contained in:
2026-09-07 14:49:29 -05:00
parent c82a0b4e3c
commit 676856dfe1
10 changed files with 1090 additions and 910 deletions
Generated
+3
View File
@@ -2143,6 +2143,9 @@ name = "vtest"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"forge_core", "forge_core",
"lazy_static",
"novaforth",
"tinyjson",
"vasm_core", "vasm_core",
"vcore", "vcore",
] ]
+3 -3
View File
@@ -2,7 +2,7 @@ mod keyboard;
use winit::{ use winit::{
dpi::LogicalSize, dpi::LogicalSize,
event::{Event, WindowEvent}, event::WindowEvent,
event_loop::{ControlFlow, EventLoop}, event_loop::{ControlFlow, EventLoop},
}; };
@@ -16,7 +16,7 @@ use vasm_core::assemble_snippet;
use vcore::cpu::CPU; use vcore::cpu::CPU;
use vcore::memory::{Memory, PeekPoke}; use vcore::memory::{Memory, PeekPoke};
use vcore::word::Word; use vcore::word::Word;
use winit::event::{DeviceEvent, DeviceId, ElementState, StartCause}; use winit::event::{ElementState, StartCause};
use winit::event_loop::ActiveEventLoop; use winit::event_loop::ActiveEventLoop;
use winit::keyboard::PhysicalKey; use winit::keyboard::PhysicalKey;
use winit::window::{Window, WindowId}; 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 { match event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
+1 -1
View File
@@ -5,7 +5,7 @@ use mlua::{Error, UserData, UserDataMethods};
use vcore::memory::{PeekPoke, PeekPokeExt}; use vcore::memory::{PeekPoke, PeekPokeExt};
use std::iter::FromIterator; use std::iter::FromIterator;
use tinyjson::JsonValue; use tinyjson::JsonValue;
use novaforth::{ROM, PRELUDE, SYMBOLS}; use novaforth::{ROM, SYMBOLS};
use vcore::opcodes::Opcode; use vcore::opcodes::Opcode;
#[mlua::lua_module] #[mlua::lua_module]
+3
View File
@@ -9,3 +9,6 @@ edition = "2021"
vcore = { path = "../vcore" } vcore = { path = "../vcore" }
vasm_core = { path = "../vasm_core" } vasm_core = { path = "../vasm_core" }
forge_core = { path = "../forge_core" } forge_core = { path = "../forge_core" }
novaforth = { path = "../novaforth" }
lazy_static = "1.4.0"
tinyjson = "2.5.1"
-904
View File
@@ -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))
+22
View File
@@ -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;
+12
View File
@@ -3,3 +3,15 @@ mod integration_tests;
#[cfg(test)] #[cfg(test)]
mod forge_tests; mod forge_tests;
#[cfg(test)]
mod novaforth_tests;
#[cfg(test)]
mod memory_item;
#[cfg(test)]
mod constants;
#[cfg(test)]
mod test_harness;
+166
View File
@@ -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"]),
}
}
}
+712
View File
@@ -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));
}
+166
View File
@@ -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
}
}