diff --git a/doc/tal.md b/doc/tal.md index 075200a..45965a2 100644 --- a/doc/tal.md +++ b/doc/tal.md @@ -6,21 +6,28 @@ TAL is a Forth like two-pass assembler language translating directly to UXN memo ## Words Words are up to 63 consecutive non-whitespace characters. -For instance `0x75786E00` (ascii UXN\0) would be one TAL "word" although its value is many bytes. -`foo`, `bar-baz` and `quix/qux` would all be examples of words. +For instance `loop`, `System`, `Mouse/x`, `my-routine` and `some_other_routine` would all be examples of words. +The UXN instructions themselves (`ADD`, `POP`, `LIT` and soforth) are all words. -Words starting with `_` are defined to be relative references. -Words starting with `,` are +Some words have special interpretations. -## Comments -Comments in TAL are written `( ... )` and support nesting. Eg. `( () )` is a valid comment. `( ( )` is not. -TAL does not have a way to "close all start comments" like Java and some other languages do. +### Opcodes -## Literals +See [the UXN documentation](./uxn.md) for a full listing of opcodes, but `BRK`, `INC`, `POP`, `NIP`, `SWP` .... `SFT` as words all mean their respective opcodes. +These opcodes may be followed with the flags `k`, `r` or `2` to set the `keep`, `return` and `short` flags. +For instance `INC2` as a word would increment a two-bit quantity at the top of the stack. +`INC2k` would keep the original value, resulting in `x x+1` as the stack values. -Hex constants are written `#[0-9a-f]{1,4}`. -For instance `#00` or `#ffff` would be valid hex constants, the first assembling to one word, the second to two. -One and two byte literal quantities may also be provided without the `#` prefix. +### Numbers + +Hexadecimal numbers written with either two or four digits. +For instance `00` would be the single word `0x00`. +`0000` is equivalent to the two words `00 00`. +UXN is little-endian, the value `0xFF00` is represented as the sequential words `FF 00`. + +To disambiguate, numbers are usually prefixed with `#`. + +### Strings Words may be captured as ASCII formatted strings. Such strings are written `"`. @@ -28,12 +35,18 @@ For instance `"foo` would cause the bytes `#66 #6f #6f #00` to be literally inse As `"` notation cannot capture whitespace, the `#20` (space), `#0a` (newline) and `#09` (tab) character constants are common. +## Comments +Comments in TAL are written `( ... )` and support nesting. Eg. `( () )` is a valid comment. `( ( )` is not. +TAL does not have a way to "close all start comments" like Java and some other languages do. + ## Brackets `[` and `]` are treated as whitespace, and may be used for visual grouping. While they have semantics in traditional Forth, they have no semantics in TAL. -## Padding +## Assembler directives + +### Padding `|` "pad-absolute" pads the resulting UXN rom to a given absolute address. For instance `|0x0000` would explicitly align the assembler's point to `0x0000`. @@ -41,7 +54,7 @@ For instance `|0x0000` would explicitly align the assembler's point to `0x0000`. `$` "pad-relative" pads the UXN rom by the specified number of words (bytes). For instance `$2` would move the assembler's point forwards two words. -## Labels +### Labels `@` defines a top-level label. For instance `@foo` would make the word `foo` a valid symbol for use elsewhere. @@ -50,7 +63,9 @@ Defining a top-level word establishes a scope within which sub-labels may be def `&bar` following `@foo` would create the label `foo/bar`. This can be used to create semantic tables. -### Example - the system device +Numbers and opcodes cannot be created as labels. + +#### Example - the system device ```tal |00 @System &vector $2 &wst $1 &rst $1 &eaddr $2 &ecode $1 &pad $1 &r $2 &g $2 &b $2 &debug $1 &halt $1 @@ -72,7 +87,7 @@ This line of code creates the following symbols: - `System/debug` at `0x000e` - `System/halt` at `0x000f` -## References +### Label References Labels may be referenced in one of seven ways: - Literal byte zero-page - `.label` @@ -100,7 +115,7 @@ For bytecode compactness, UXN programs tend to use computed rather than absolute The difference between single and double word references is critical, because the `LDR` instruction is a computed relative load, whereas `LDA` is an absolute short address load. -## Includes +### Includes TAL files can include other files by writing `~`. For instance the `uxnasm.tal` file writes `~projects/library/string.tal` to include implementations of string functions. As with other preprocessor and assembler languages, TAL does not support namespacing, renaming or selective importing. diff --git a/doc/uxn.md b/doc/uxn.md index 95f1116..ef624a8 100644 --- a/doc/uxn.md +++ b/doc/uxn.md @@ -34,7 +34,7 @@ Effects are written using forth-style notation `inputs -- outputs` | Opcode | Memonic | Long name | Data stack effect | Control stack effect | PC effect | Memory effect | |--------|---------|--------------------------|---------------------------------------------|----------------------|---------------------|-------------------| -| 0x00 | BRK | Break | -- | | | | +| 0x00 | BRK | Break | -- | -- | | | | 0x01 | INC | Increment | a -- a+1 | | | | | 0x02 | POP | Pop | a -- | | | | | 0x03 | NIP | Nip | b a -- a | | | | diff --git a/doc/varvara.md b/doc/varvara.md index 76ced10..6879e1f 100644 --- a/doc/varvara.md +++ b/doc/varvara.md @@ -1,6 +1,6 @@ # Varvara -[Varvara](https://wiki.xxiivv.com/site/varvara.html) is a personal computer, using the [uxn](./uxn.md) instruction set, programmed in [tal](./tal.md) +[Varvara](https://wiki.xxiivv.com/site/varvara.html) is a personal computer, using the [uxn](./uxn.md) instruction set, programmed in [tal](./tal.md). UXN presents 16 I/O ports via the `DEI` and `DEO` instructions. Each port consists of 16 words of memory, and has its own I/O memory mapping behavior. @@ -8,51 +8,66 @@ Note that while other memory read and write instructions can interface with port The Varvara computer presents the following canonical port mappings - -- `0x0`, System device -- `0x1`, Console device (text output) -- `0x2`, Screen device (bitmap output) -- `0x3`, Audio device (`audio0`) -- `0x4`, Audio device (`audio1`) -- `0x5`, Audio device (`audio2`) -- `0x6`, Audio device (`audio3`) -- `0x7`, Unused -- `0x8`, D-pad controller -- `0x9`, Mouse -- `0xA`, File (`file0`) -- `0xB`, File (`file1`) -- `0xC`, Datetime -- `0xD`, Unused -- `0xE`, Unused -- `0xF`, Unused +- `0x0000`, System device +- `0x0010`, Console device (text output) +- `0x0020`, Screen device (bitmap output) +- `0x0030`, Audio device (`audio0`) +- `0x0040`, Audio device (`audio1`) +- `0x0050`, Audio device (`audio2`) +- `0x0060`, Audio device (`audio3`) +- `0x0070`, Unused +- `0x0080`, D-pad controller +- `0x0090`, Mouse +- `0x00A0`, File (`file0`) +- `0x00B0`, File (`file1`) +- `0x00C0`, Datetime +- `0x00D0`, Unused +- `0x00E0`, Unused +- `0x00F0`, ROM metadata on some implementations -``` -|00 @System &vector $2 &wst $1 &rst $1 &eaddr $2 &ecode $1 &pad $1 &r $2 &g $2 &b $2 &debug $1 &halt $1 -|10 @Console &vector $2 &read $1 &pad $5 &write $1 &error $1 -|20 @Screen &vector $2 &width $2 &height $2 &auto $1 &pad $1 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1 -|30 @Audio0 &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 -|40 @Audio1 &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 -|50 @Audio2 &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 -|60 @Audio3 &vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 -|80 @Controller &vector $2 &button $1 &key $1 &func $1 -|90 @Mouse &vector $2 &x $2 &y $2 &state $1 &pad $3 &scrollx $2 &scrolly $2 -|a0 @File0 &vector $2 &success $2 &stat $2 &delete $1 &append $1 &name $2 &length $2 &read $2 &write $2 -|b0 @File1 &vector $2 &success $2 &stat $2 &delete $1 &append $1 &name $2 &length $2 &read $2 &write $2 -|c0 @DateTime &year $2 &month $1 &day $1 &hour $1 &minute $1 &second $1 &dotw $1 &doty $2 &isdst $1 -``` +Note that while ports have memory addresses, the 0 page is normal memory. +The `DEI` and `DEO` port I/O instructions have memory effects, but loads/stores to port mapped memory does not have I/O effects. -Varvara executes a program starting at `0x0100` until the `BRK` instruction occurs. -`BRK` does not halt the machine (that would be `%halt { #01 LIT .System/halt DIO }`). +Varvara executes ROMS starting at `0x0100` until the `BRK` instruction occurs. +`BRK` does not halt the machine. Instead it signals that the present 'thread' of execution has completed, and returns control so that an interrupt handler can occur. -The main or initialization program must register interrupt handlers before `BRK`. +Machine termination can be accomplished with `%BYE { #01 #000F DEO BRK }` which writes `#01` to the `System/state` port, marking the machine as halted and breaks. Varvara provides 'interrupt' based I/O. The 'vector' of a given device is a program point. That point will be invoked with no stack arguments when an event on the device occurs. -Interrupt handlers execute until they terminate using the `BRK` instruction. -Varvara enqueues interrupts and will not fire another until the one under execution has completed. +Threads of execution precede until they yield using the `BRK` instruction. -The screen device's vector is a clock which ticks at 60hz. -The mouse device's vector likewise fires when a mouse event such as movement or a button press occurs. -The controller device similarly fires on input changes. +Typically the 'main' program starting at `0x0100` will initialize handler vectors and then `BRK` to enable I/O. + +**WARNING**: `BRK` does not alter or reset either stack. +It is possible to perform (deliberate or accidental) communication between vectors due to this. + +## Devices + +### The system [themes](https://wiki.xxiivv.com/site/theme.html) + + +### The console + +### The screen + +The screen device's vector is a clock which ticks at 60hz. + +### Audio + +### The controller +The controller device similarly fires on input changes. + +### The mouse +The mouse device's vector likewise fires when a mouse event such as movement or a button press occurs. + +### Files + +### Datetime + +### Metadata + +[metadata](https://wiki.xxiivv.com/site/metadata.html) diff --git a/src/bin/main.rs b/src/bin/main.rs index e7a11a9..3b013e6 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,3 +1,6 @@ +use uxn::Uxn; + fn main() { - println!("Hello, world!"); + let vm = Uxn::new(); + println!("{:?}", vm); } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b42088e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,158 @@ +pub mod memory; +pub mod stack; + +use std::rc::Rc; +use std::*; + +use memory::TrivialMemory; +use stack::Stack; + +trait Device: std::fmt::Debug { + /** + * Callbacks used by UXN to do input from a "device". + */ + fn dei1(&self, vm: &mut Uxn, port: u8) -> u8; + fn dei2(&self, vm: &mut Uxn, port: u8) -> u16; + + /** + * Callback used by UXN to do output through a "device". + */ + fn deo1(&mut self, vm: &mut Uxn, port: u8, val: u8); + fn deo2(&mut self, vm: &mut Uxn, port: u8, val: u16); +} + +/** + * The null device does nothing when reading or writing. + */ +#[derive(Debug)] +struct NullDevice {} + +impl NullDevice { + pub fn new() -> NullDevice { + NullDevice {} + } +} + +impl Device for NullDevice { + fn dei1(&self, vm: &mut Uxn, port: u8) -> u8 { + 0 + } + fn dei2(&self, vm: &mut Uxn, port: u8) -> u16 { + 0 + } + fn deo1(&mut self, vm: &mut Uxn, port: u8, val: u8) {} + fn deo2(&mut self, vm: &mut Uxn, port: u8, val: u16) {} +} + +/** + * The system device + */ +#[derive(Debug)] +struct SystemDevice { + buffer: [u8; 0xF], +} + +impl SystemDevice { + fn new() -> SystemDevice { + SystemDevice { buffer: [0; 0xF] } + } +} + +impl Device for SystemDevice { + fn dei1(&self, vm: &mut Uxn, port: u8) -> u8 { + let slot = port & 0xF; + match slot { + 0x2 => vm.wst.idx, + 0x3 => vm.rst.idx, + x => self.buffer[x as usize], + } + } + fn dei2(&self, vm: &mut Uxn, port: u8) -> u16 { + return ((self.dei1(vm, port) as u16) << 8) + self.dei1(vm, port + 1) as u16; + } + fn deo1(&mut self, vm: &mut Uxn, port: u8, val: u8) { + let slot = port & 0xF; + match slot { + 0x2 => vm.wst.idx = val, + 0x3 => vm.rst.idx = val, + x => self.buffer[x as usize] = val, + } + } + fn deo2(&mut self, vm: &mut Uxn, port: u8, val: u16) { + let slot = port & 0xF; + assert!(slot < 0xF, "Double-write beyond the end of the device"); + match slot { + 0x2 => panic!("The data stack is single-width, panic on spurious double-write"), + 0x3 => panic!("The return stack is single-width, panic on spurious double-write"), + x => { + let [high, low] = val.to_be_bytes(); + self.deo1(vm, x, high); + self.deo1(vm, x + 1, low) + } + } + } +} + +#[derive(Debug)] +pub struct Uxn { + memory: Box, + // Note: Using Rc so we can start with many NullDevs and replace them + devices: [Box; 16], + pc: u16, // Program counter + wst: Stack, // Data stack + rst: Stack, // Return stack pointer +} + +impl Uxn { + pub fn new() -> Uxn { + Uxn { + memory: Box::new(TrivialMemory::new()), + devices: [ + Box::new(SystemDevice::new()), // #00 + Box::new(NullDevice::new()), // #01 + Box::new(NullDevice::new()), // #02 + Box::new(NullDevice::new()), // #03 + Box::new(NullDevice::new()), // #04 + Box::new(NullDevice::new()), // #05 + Box::new(NullDevice::new()), // #06 + Box::new(NullDevice::new()), // #07 + Box::new(NullDevice::new()), // #08 + Box::new(NullDevice::new()), // #09 + Box::new(NullDevice::new()), // #0a + Box::new(NullDevice::new()), // #0b + Box::new(NullDevice::new()), // #0c + Box::new(NullDevice::new()), // #0d + Box::new(NullDevice::new()), // #0e + Box::new(NullDevice::new()), // #0f + ], + pc: 0x0100, + wst: Stack::new(), + rst: Stack::new(), + } + } + + pub fn is_halted(&mut self) -> bool { + let dev = self.devices[0].as_mut(); + dev.dei1(self, 0x0f) != 0 + } + + pub fn dei1(&mut self, port: u8) -> u8 { + let dev = self.devices[(port & 0xF0 >> 4) as usize]; + dev.dei1(self, port) + } + + pub fn dei2(&mut self, port: u8) -> u16 { + let dev = self.devices[(port & 0xF0 >> 4) as usize]; + dev.dei2(self, port) + } + + pub fn deo1(&mut self, port: u8, val: u8) { + let dev = self.devices[(port & 0xF0 >> 4) as usize].as_mut(); + dev.deo1(self, port, val) + } + + pub fn deo2(&mut self, port: u8, val: u16) { + let dev = self.devices[(port & 0xF0 >> 4) as usize].as_mut(); + dev.deo2(self, port, val) + } +} diff --git a/src/memory.rs b/src/memory.rs new file mode 100644 index 0000000..e6f8e08 --- /dev/null +++ b/src/memory.rs @@ -0,0 +1,64 @@ +#[derive(Debug)] +pub enum MemoryError { + AddressOverflow, + AddressUnderflow, +} + +/** + * An interface describing how the VM can read from and write to memories. + * + * The trick here is that get2/set2 COULD overflow, consider get2(0xFFFF). + */ +pub trait Memory { + fn get1(&self, address: u16) -> Result; + fn get2(&self, address: u16) -> Result; + fn set1(&mut self, address: u16, val: u8) -> Result<(), MemoryError>; + fn set2(&mut self, address: u16, val: u16) -> Result<(), MemoryError>; +} + +/** + * The obvious trivially correct impl of memory using One Big Buffer. + */ +#[derive(Debug)] +pub struct TrivialMemory { + buffer: [u8; 0xFFFF], +} + +impl TrivialMemory { + pub fn new() -> TrivialMemory { + TrivialMemory { + buffer: [0; 0xFFFF], + } + } +} + +impl Memory for TrivialMemory { + fn get1(&self, address: u16) -> Result { + return Ok(self.buffer[address as usize]); + } + + fn get2(&self, address: u16) -> Result { + if address == 0xFFFFu16 { + Err(MemoryError::AddressOverflow) + } else { + return Ok(((self.buffer[address as usize] as u16) << 8) + + self.buffer[address as usize + 1] as u16); + } + } + + fn set1(&mut self, address: u16, val: u8) -> Result<(), MemoryError> { + self.buffer[address as usize] = val; + Ok(()) + } + + fn set2(&mut self, address: u16, val: u16) -> Result<(), MemoryError> { + if address == 0xFFFFu16 { + Err(MemoryError::AddressOverflow) + } else { + let [high, low] = val.to_be_bytes(); + self.set1(address, high); + self.set1(address + 1, low); + Ok(()) + } + } +} diff --git a/src/stack.rs b/src/stack.rs new file mode 100644 index 0000000..f496201 --- /dev/null +++ b/src/stack.rs @@ -0,0 +1,58 @@ +use std; +use std::result::Result; + +#[derive(Debug)] +pub struct Stack { + buff: [u8; 0xFF], + pub idx: u8, +} + +pub enum StackError { + StackOverflow, + StackUnderflow, +} + +impl Stack { + pub fn new() -> Stack { + Stack { + buff: [0; 0xFF], + idx: 0, + } + } + + pub fn push1(&mut self, val: u8) -> Result<(), StackError> { + if self.idx == 255 { + Err(StackError::StackOverflow) + } else { + self.buff[self.idx as usize] = val; + self.idx += 1; + Ok(()) + } + } + + pub fn push2(&mut self, val: u16) -> Result<(), StackError> { + if self.idx > 254 { + Err(StackError::StackOverflow) + } else { + val.to_le_bytes().map(|x| self.push1(x).ok()); + Ok(()) + } + } + + pub fn pop1(&mut self) -> Result { + if self.idx == 0 { + Err(StackError::StackUnderflow) + } else { + self.idx -= 1; + Ok(self.buff[self.idx as usize]) + } + } + + pub fn pop2(&mut self) -> Result { + if self.idx < 2 { + Err(StackError::StackUnderflow) + } else { + Ok(((self.pop1()? as u16) << 8) + self.pop1()? as u16) + } + } +} diff --git a/src/uxn.rs b/src/uxn.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/uxn.rs +++ /dev/null @@ -1 +0,0 @@ -