[NO TESTS] WIP

This commit is contained in:
Reid 'arrdem' McKenzie 2022-12-11 22:50:41 -07:00
parent 48cd076364
commit 12d657097c
8 changed files with 370 additions and 58 deletions

View file

@ -6,21 +6,28 @@ TAL is a Forth like two-pass assembler language translating directly to UXN memo
## Words ## Words
Words are up to 63 consecutive non-whitespace characters. 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. For instance `loop`, `System`, `Mouse/x`, `my-routine` and `some_other_routine` would all be examples of words.
`foo`, `bar-baz` and `quix/qux` 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. Some words have special interpretations.
Words starting with `,` are
## Comments ### Opcodes
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.
## 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}`. ### Numbers
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. 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. Words may be captured as ASCII formatted strings.
Such strings are written `"<word>`. Such strings are written `"<word>`.
@ -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. 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 ## Brackets
`[` and `]` are treated as whitespace, and may be used for visual grouping. `[` 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. While they have semantics in traditional Forth, they have no semantics in TAL.
## Padding ## Assembler directives
### Padding
`|<number>` "pad-absolute" pads the resulting UXN rom to a given absolute address. `|<number>` "pad-absolute" pads the resulting UXN rom to a given absolute address.
For instance `|0x0000` would explicitly align the assembler's point to `0x0000`. 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`.
`$<number>` "pad-relative" pads the UXN rom by the specified number of words (bytes). `$<number>` "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. For instance `$2` would move the assembler's point forwards two words.
## Labels ### Labels
`@<word>` defines a top-level label. `@<word>` defines a top-level label.
For instance `@foo` would make the word `foo` a valid symbol for use elsewhere. 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`. `&bar` following `@foo` would create the label `foo/bar`.
This can be used to create semantic tables. 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 ```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 |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/debug` at `0x000e`
- `System/halt` at `0x000f` - `System/halt` at `0x000f`
## References ### Label References
Labels may be referenced in one of seven ways: Labels may be referenced in one of seven ways:
- Literal byte zero-page - `.label` - 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. 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 `~<filename>`. TAL files can include other files by writing `~<filename>`.
For instance the `uxnasm.tal` file writes `~projects/library/string.tal` to include implementations of string functions. 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. As with other preprocessor and assembler languages, TAL does not support namespacing, renaming or selective importing.

View file

@ -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 | | 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 | | | | | 0x01 | INC | Increment | a -- a+1 | | | |
| 0x02 | POP | Pop | a -- | | | | | 0x02 | POP | Pop | a -- | | | |
| 0x03 | NIP | Nip | b a -- a | | | | | 0x03 | NIP | Nip | b a -- a | | | |

View file

@ -1,6 +1,6 @@
# Varvara # 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. 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. 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 - The Varvara computer presents the following canonical port mappings -
- `0x0`, System device - `0x0000`, System device
- `0x1`, Console device (text output) - `0x0010`, Console device (text output)
- `0x2`, Screen device (bitmap output) - `0x0020`, Screen device (bitmap output)
- `0x3`, Audio device (`audio0`) - `0x0030`, Audio device (`audio0`)
- `0x4`, Audio device (`audio1`) - `0x0040`, Audio device (`audio1`)
- `0x5`, Audio device (`audio2`) - `0x0050`, Audio device (`audio2`)
- `0x6`, Audio device (`audio3`) - `0x0060`, Audio device (`audio3`)
- `0x7`, Unused - `0x0070`, Unused
- `0x8`, D-pad controller - `0x0080`, D-pad controller
- `0x9`, Mouse - `0x0090`, Mouse
- `0xA`, File (`file0`) - `0x00A0`, File (`file0`)
- `0xB`, File (`file1`) - `0x00B0`, File (`file1`)
- `0xC`, Datetime - `0x00C0`, Datetime
- `0xD`, Unused - `0x00D0`, Unused
- `0xE`, Unused - `0x00E0`, Unused
- `0xF`, Unused - `0x00F0`, ROM metadata on some implementations
``` Note that while ports have memory addresses, the 0 page is normal memory.
|00 @System &vector $2 &wst $1 &rst $1 &eaddr $2 &ecode $1 &pad $1 &r $2 &g $2 &b $2 &debug $1 &halt $1 The `DEI` and `DEO` port I/O instructions have memory effects, but loads/stores to port mapped memory does not have I/O effects.
|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
```
Varvara executes a program starting at `0x0100` until the `BRK` instruction occurs. Varvara executes ROMS starting at `0x0100` until the `BRK` instruction occurs.
`BRK` does not halt the machine (that would be `%halt { #01 LIT .System/halt DIO }`). `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. 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. Varvara provides 'interrupt' based I/O.
The 'vector' of a given device is a program point. 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. 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. Threads of execution precede until they yield using the `BRK` instruction.
Varvara enqueues interrupts and will not fire another until the one under execution has completed.
The screen device's vector is a clock which ticks at 60hz. Typically the 'main' program starting at `0x0100` will initialize handler vectors and then `BRK` to enable I/O.
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. **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) [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)

View file

@ -1,3 +1,6 @@
use uxn::Uxn;
fn main() { fn main() {
println!("Hello, world!"); let vm = Uxn::new();
println!("{:?}", vm);
} }

158
src/lib.rs Normal file
View file

@ -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<TrivialMemory>,
// Note: Using Rc so we can start with many NullDevs and replace them
devices: [Box<dyn Device>; 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)
}
}

64
src/memory.rs Normal file
View file

@ -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<u8, MemoryError>;
fn get2(&self, address: u16) -> Result<u16, MemoryError>;
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<u8, MemoryError> {
return Ok(self.buffer[address as usize]);
}
fn get2(&self, address: u16) -> Result<u16, MemoryError> {
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(())
}
}
}

58
src/stack.rs Normal file
View file

@ -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<u8, StackError> {
if self.idx == 0 {
Err(StackError::StackUnderflow)
} else {
self.idx -= 1;
Ok(self.buff[self.idx as usize])
}
}
pub fn pop2(&mut self) -> Result<u16, StackError> {
if self.idx < 2 {
Err(StackError::StackUnderflow)
} else {
Ok(((self.pop1()? as u16) << 8) + self.pop1()? as u16)
}
}
}

View file

@ -1 +0,0 @@