initial upload

This commit is contained in:
Luxferre
2025-04-22 20:02:27 +03:00
commit 6bc0a916eb
8 changed files with 977 additions and 0 deletions
+339
View File
@@ -0,0 +1,339 @@
mu808: an even more ultralight numeric VM designed to run everywhere
====================================================================
mu808 (always lowercase, pronounced as _micro-bob_) is a minimalist virtual
machine specification that is based on the 1V0 and 808UL ideas but has an
even leaner instruction set and is even easier to implement in any modern
or even old programming language.
Overview
--------
mu808 is a Harvard-architecture virtual machine with separate memory space for
programs and data. The only available data type is a floating point number,
although some implementations may use fixed point numbers instead.
Unlike 808UL, mu808 strictly defines support of up to 16384 data cells and
up to 16383 program steps, with the program step 0 being unavailable and the
data cell 0 always returning 0.
On the machine start, both memory areas are initialized with zeroes and the
interactive mode is entered which accepts instructions from the platform's
standard input.
### Overall features
* 16 runtime instructions (from 0 to 15, including NOP as instruction 0);
* ability to enter integer commands (positive and negative) into program memory;
* ability to enter floating (or fixed) point numbers (positive and negative) at
program runtime;
* immutability of the data address 0 (must always return 0 when accessed).
The following things are implementation-dependent:
* instruction and data internal representation;
* tracing capabilities;
* runlimit (the amount of program steps before forcefully halting the program).
mu808 instruction set
---------------------
Unlike 808UL, mu808 only has a core instruction set. Also, regardless of the
instruction number, all instructions strictly contain five numbers separated
by a whitespace:
* `[lno]` - instruction number,
* `[cmd]` - command,
* `[arg1]` - argument 1,
* `[arg2]` - argument 2,
* `[arg3]` - argument 3.
Any mu808 implementation must not distinguish between a whitespace and a newline
character: both of them, if supported, must be used to delimit individual parts
of an instruction and not instructions as a whole. Instructions as a whole,
however, are separated naturally by reading five delimited numbers at a time.
This rule applies to the plaintext format of the machine code. See the section
about mu808 file formats to find out about the binary machine code format.
### Instruction number semantics
* Any positive integer: the instruction is written into the program memory under
this address (any previous instruction at this address is overwritten).
* 0: the instruction is NOT written into the program memory and is immediately
executed instead. If the instruction is a jump instruction that jumps into a
valid positive address, program execution in the program memory begins until
the program memory is exhausted.
* -1: displays a range of instructions from address `[cmd]` to `[arg1]` (incl).
* -2: if `[cmd]` is 0, clears a range of instructions from address `[arg1]` to
`[arg2]` (inclusively), otherwise clears a range of data from address `[cmd]`
to `[arg2]` (inclusively).
* -3: if supported, turns off execution tracing output if the `[cmd]` value is
equal to 0, otherwise turns it on.
* -4: if supported, changes the program runlimit to the value of `[cmd]`.
* -5: if supported, the VM exits to the OS environment, otherwise resets to
its initial state (both memory areas filled with zeroes).
For the data entry and display, a runtime command is required instead
(see below for details).
### Runtime command set
The following commands are required for all mu808 ports and implementations.
The following notation is in place for this list:
* `A1` - address passed as the first command argument;
* `A2` - address passed as the second command argument;
* `A3` - address passed as the third command argument;
* `V1` - numeric value stored at the address `A1`;
* `V2` - numeric value stored at the address `A2`;
* `V3` - numeric value stored at the address `A3`.
The mu808 runtime command list follows along with their mnemonics.
* 0 (NOP): No operation. The instruction is ignored regardless of parameters.
* 1 (JMP): Jump. This is the most complicated operation in the list, see the
next section ("Jump operation semantics") for the full explanation how it
works.
* 2 (IAT): Indirect addressing toggle. This instruction overrides the next
instruction by providing `V1`, `V2` and `V3` as the parameters for the next
instruction's command. The actual command parameters provided with the next
instruction will be ignored.
* 3 (OUT): Output. Print the range of values stored from the address `A1` to
the address `A2` (inclusively) to the port number in `A3` (0 is the platform's
standard output).
* 4 (INP): Input. Prompt the user to enter the range of values to be stored from
the address `A1` to the address `A2` (inclusively) from the port number in
`A3` (0 is the platform's standard input).
* 5 (SET): Set value at address. Interpret `A1` and `A2` as two direct integer
values and then set the value at `A3` to
`(A1 mod 10000) + (A2 mod 10000) / 10000`.
* 6 (CPY): Direct/indirect value copy. If `A1` is 0, then set the value at `A3`
to `A2`. If `A1` is 1, set the value at `A3` to `V2`. Otherwise, set the value
at `V3` to the contents of the address `V2` (converted to integer).
* 7 (FMA): Fused multiply-add. Set the value at `A3` to `V1 + (V2 * V3)`.
* 8 (SUB): Subtract. Set the value at `A3` to `V1 - V2`.
* 9 (DIV): Divide. Set the value at `A3` to 0 if `V2` is 0, otherwise set it to
`V1 / V2`.
* 10 (MDF): Modulo/floor. Interpret `V1` and `V2` as integers and set the value
at `A3` to `V1 mod V2` if `V2` is not 0, otherwise set the result to the
integer part of `V1`.
* 11 (ABS): Absolute value. Set the value at `A3` to `|V2|`.
* 12 (SQR): Square root. Set the value at `A3` to the square root of `|V2|`.
* 13 (NEL): Natural exponent/logarithm. If the value of `A1` is 0, set the value
at `A3` to `e ** V2`, otherwise set it to `ln |V2|`.
* 14 (TRI): Trigonometric function. If the value of `A1` is 0, set the value at
`A3` to `sin V2`, if the value of `A1` is 1, set the value at `A3` to
`cos V2`, otherwise set it to `arctg V2`. The parameters are given in radians.
* 15 (RND): Random number. Set the value at `A3` to a random integer number
between `V1` and `V2` (inclusively).
### Jump operation semantics
The jump operation (instruction command 1) in mu808 is almost identical to the
corresponding operation in 808UL, which, in turn, was directly modeled after
the corresponding operation in the 1V0 TZ IV variant. It allows for all kinds
of conditional and unconditional, direct and indirect jumps based on three
input parameters.
Using the same notation as above, the jump operation semantics is as follows:
* If `V2` == 0 and `A1` == 0, or
* if `V2` > 0 and `A1` == 1, or
* if `V2` < 0 and `A1` == 2, or
* if `V2` >= 0 and `A1` == 3, or
* if `V2` <= 0 and `A1` == 4, or
* if `V2` != 0 and `A1` == 5, or
* if `A1` == 6,
then jump to the address `A3`.
* If `V2` == 0 and `A1` == 7, or
* if `V2` > 0 and `A1` == 8, or
* if `V2` < 0 and `A1` == 9, or
* if `V2` >= 0 and `A1` == 10, or
* if `V2` <= 0 and `A1` == 11, or
* if `V2` != 0 and `A1` == 12, or
* if `A1` == 13,
then jump to the address `V3` (converting it to an integer first).
**Note** that the order of operands has changed compared to the 1V0/808UL
specifications: here, it's `[selector] [value] [jumpaddr]` as opposed to
`[value] [jumpaddr] [selector]`. This has been done for more convenient
jump programming by selecting the condition as the first operand.
### Port input and output
For the commands 3 and 4, mu808 supports optional non-zero port numbers, where
port 1 is the character output port and port 2 is the character input port
respectively. Additional custom ports may be implementation-defined.
File and data formats related to mu808
--------------------------------------
This specification also defines several file formats that can be used for mu808
programming process.
### Plain text machine code format (MU8)
This is the main format to store and enter mu808 programs in. It essentially
duplicates what's being entered into the mu808 REPL, with the exception of the
negative instruction numbers. Unlike 808UL, it only allows specifying digits and
whitespace in the file. Implementations are only required to support the space
character (32, 0x20) as the delimiter, but should also support newline, tabs and
other kinds of whitespace in the same way.
The recommended file suffix for mu808 plain text machine code is .mu8.
### Binary machine code format (MU8B)
Unlike 808UL, mu808 also defines a MU8B binary format to store every instruction
in 64 bits of data in the following order (for easier processing):
* 4 higher bits are unused,
* 4 bits are used for the command opcode (0 to 15),
* 14 bits are used for the instruction number (can't be 0),
* 14 bits are used for the command argument 1,
* 14 bits are used for the command argument 2,
* 14 bits are used for the command argument 3.
Hence, every instruction can only contain the number from 0 to 16383 in any of
its command arguments, and from 1 to 16383 in its instruction number field.
The recommended file suffix for mu808 binaries is .mu8b or .bin. Implementors
are encouraged (but not required) to provide tools for conversion between the
binary and text representations of mu808 machine code.
When exporting the entire program memory to the MU8B format, NOPs may or may
not be omitted. If a mu808 implementation supports MU8B file import, it must
behave the same way as if loading MU8 (plain text) machine code with the same
instructions or entering them manually.
### Assembly source code file format (MU8A)
In addition to direct machine code in the plain text or MU8B formats, the mu808
VM also allows using an assembly-like language to write programs using labels
and the above mnemonics (case-insensitive). The recommended file suffix is
.mu8a.
An assembly line looks like this (the optional parts are enclosed in square
brackets): `[:lbl] MNEMONIC arg1 arg2 arg3 [;comment]`. Labels are optional but
must start with a colon (`:`) and be on the same line before the instruction
they label. The mnemonics are specified above in the core opcode list.
Here, the exact assembly algorithm is specified step-by-step for each line in
the MU8A assembly file to convert it into a plain text based machine code
representation:
1. Remove all comments (starting with `;` until the end of the line).
2. Record the current line number N (not counting completely empty lines),
starting with 1.
3. Split the line into space-delimited fields (1-based numbering as well).
4. Check if the first field starts with `:`. If so, mark the mapping between
the field text and the number N, then remove the field from the set
(so that the field 2 becomes field 1 and so on).
5. If the field 1 is not already a number, replace the field 1 mnemonic text
with the numeric opcode if it can be found. If it's not a number and the
mnemonic cannot be found, report an error and halt the process.
6. Prepend N and the space to the current line and write the result into the
target file.
7. After the steps 1 to 6 are complete for every line, replace every label
occurrence in the mapping with the corresponding number in the target file.
Examples
--------
The [examples](examples/) subdirectory contains several MU8A source code file
examples for mu808 which are ports from the same 808UL example programs, namely:
* [Compound interest calculator](examples/compound.mu8a),
* [Linear regression calculator](examples/linreg.mu8a),
* [Hellorld!](examples/hellorld.mu8a) (a tribute to @UsagiElectric),
* [Bulls and Cows game](examples/moo.mu8a),
* [Lunar Lander game](examples/lunar.mu8a).
You can assemble them using any of the reference assemblers provided within the
repository, or even by hand (by numbering lines, resolving the labels and
replacing mnemonics with corresponding opcodes). The "Hellorld!" example only
works on the platforms supporting I/O port 1 (character output).
Reference implementations
-------------------------
### mu808 VM reference implementations
* [Python 3 implementation](mu808.py): the first one where all prototyping and
initial spec development has been done. Also compatible with MicroPython an
tested on it. Supports the entire documented mu808 instruction set, including
the two extended I/O ports (requires termios library to work with the
character input port correctly on desktop OSes but falls back to a simple
stream file read in case the library is not found). Accepts a file name to
preload a MU8 (plain text) machine code program from the OS command line.
### Assembler reference implementations
* [Python 3](mu808asm.py): supports assembling MU8A source files into both
plain text (MU8) and binary (MU8B) machine code formats. Also supports
disassembling MU8 and MU8B files into their MU8A sources and converting
machine code between MU8 and MU8B formats.
These lists are going to be expanded as soon as new reference implementations
appear.
FAQ
---
### Why another 1V0 derivative that's even code-incompatible with 808UL?
808UL has served its purpose of fulfilling the author's vision on what an
1V0-like (virtual) system should look like. However, its ISA still somewhat
lacked rigidity and refinedness in terms of an optimal functionality
distribution: there were a lot of instructions that didn't use all three
command arguments, there were some instructions that could be parameterized
better to avoid functionality duplication, and dedicated port I/O subcommands
seemed like a hack on top of already available I/O routines. The mu808 ISA does
a pretty good job at polishing those bits while retaining all the main features
of the 808UL specification, also adding much stricter constraints on how the
operating environment is to be organized.
### What can be considered a minimum compatible mu808 VM implementation?
A minimum implementation must implement all runtime instruction commands (from 0
to 15 inclusive) and the interactive loop that accepts the following instruction
numbers: any positive number, 0, -1, -2 and -5.
A minimum implementation is required to support floating point numbers as a data
type, or, in case it's technically impossible, fixed point numbers with at least
two digits after the decimal point (four or more recommended).
### 808UL supported sparse instructions, is this feature gone in mu808?
No, not at all. It's the MU8A assembly language that doesn't (yet) support this
feature and autonumbers instructions based on their position in the source code.
When writing machine code directly, you can specify any instruction numbers you
like in the program.
### Why combine multiplication and addition into a single instruction command?
Because it is faster on modern architectures if both of them are required, and
doesn't add any complexity if only one of them is. Additionally, the FMA
operation is included in IEEE 754-2008, and all reference mu808 implementations
are using it in case the target platform allows them to.
### How to do simple addition and simple multiplication, given only the FMA?
It might seem inconvenient at first, but the FMA operation is quite capable.
First off, you already have a constant 0 always stored in 0, but it's advisable
to store a constant 1 into some unused location, like 99. You can do this e.g.
with the `set 1 0 99` operation. Then, let's go with three cases (assuming we
have stored 1 into the data memory location 99):
1. Incrementing a memory location is as easy as `fma 99 99 [loc]`.
2. Adding two numbers at loc1 and loc2 into loc2: `fma [loc1] 99 [loc2]`.
3. Multiplying two numbers at loc1 and loc2 into loc2: `fma 0 [loc1] [loc2]`.
The only inconvenience with the FMA operation is that you need to sacrifice one
of the operand locations to store the result. If you want to fully emulate the
808UL's behavior, then you have to copy one of the operands into the target
memory location first and then use it as the last parameter to the FMA command.
Credits
-------
Created by Luxferre in 2025, released into public domain with no warranties.
+13
View File
@@ -0,0 +1,13 @@
; A simple compound interest calculator in MU8A for mu808 VM
; Prompts for the percentage and then for the period, outputs the resulting multiplier
; Created by Luxferre in 2025, released into public domain
set 100 0 1 ; store the constant 100 at loc 1
inp 2 3 0 ; prompt for the percentage into loc 2 and the period into loc 3
div 2 1 4 ; divide the percentage value at loc 2 by the constant at loc 1 into loc 4
set 1 0 5 ; set the loc 5 to 1
fma 5 4 5 ; add the constant at loc 5 to the value at loc 4 into loc 5
nel 1 5 5 ; replace loc 5 with its ln
fma 0 3 5 ; replace loc 5 with loc 3 * ln loc 5
nel 0 5 5 ; replace loc 4 with its nexp
out 5 5 0 ; output the resulting value
+19
View File
@@ -0,0 +1,19 @@
set 72 0 1 ; fill in the data bytes from 1 to 10
set 101 0 2
set 108 0 3
set 108 0 4
set 111 0 5
set 114 0 6
set 108 0 7
set 100 0 8
set 33 0 9
set 10 0 10 ; end the string with an LF character for newline
set 1 0 50 ; set the constant 1 to memory loc 50
set 1 0 10 ; set the first address to 1 at loc 10
set 10 0 11 ; set the counter variable to 10 at loc 11
:lp set 12 0 12 ; prepare the loc 12 with its own address
cpy 2 10 12 ; load the contents of the current address at loc 10 into loc 12
out 12 12 1 ; output the character at the current address to port 1
sub 11 50 11 ; decrement the counter at loc 11
fma 50 50 10 ; increment the current address at loc 10
jmp 1 11 :lp ; jump to the loop start if the counter is over zero
+55
View File
@@ -0,0 +1,55 @@
; Linear regression calculator in MU8A for mu808 VM
; Enter the pairs number by number, end with 0,0 pair
; The program will then output A and B parameters of A + Bx
; linear function and then the correlation coefficient r
; Created by Luxferre in 2025, released into public domain
set 0 0 10 ; x-sum to loc 10
set 0 0 11 ; y-sum to loc 11
set 0 0 12 ; x-square-sum to loc 12
set 0 0 13 ; y-square-sum to loc 13
set 0 0 14 ; xy-sum to loc 14
set 0 0 15 ; n to loc 15
set 1 0 50 ; constant 1 to loc 50
:lp inp 1 2 0 ; loop start; input xi and yi into loc 1 and loc 2
cpy 1 1 3 ; copy x into loc 3
fma 0 3 3 ; save x-squared into loc 3
cpy 1 2 4 ; copy y into loc 4
fma 0 4 4 ; save y-squared into loc y
cpy 1 2 5 ; copy y into loc 5
fma 0 1 5 ; save xy into loc 5
fma 1 50 10 ; update x-sum (Sx) => loc 10
fma 2 50 11 ; update y-sum (Sy) => loc 11
fma 3 50 12 ; update x-square-sum (Sxx) => loc 12
fma 4 50 13 ; update y-square-sum (Syy) => loc 13
fma 5 50 14 ; update xy-sum (Sxy) => loc 14
fma 50 50 15 ; increment n at loc 15
cpy 1 3 6 ; copy x-squared into loc 6
fma 4 50 6 ; add x-squared and y-squared into loc 6
jmp 1 6 :lp ; loop back to the input if the square sum is over zero
sub 15 50 15 ; decrement last n at loc 15 to omit the (0,0) input
cpy 1 15 1 ; copy n to loc 1
fma 0 14 1 ; n * Sxy => loc 1
cpy 1 11 5 ; copy Sy to loc 5
fma 0 10 5 ; Sx * Sy => loc 5
sub 1 5 6 ; n * Sxy - Sx * Sy => loc 6 (to be stored for r calculation)
cpy 1 15 3 ; copy n to loc 3
fma 0 12 3 ; n * Sxx => loc 3
cpy 1 10 4 ; copy Sx to loc 4
fma 0 10 4 ; Sx squared => loc 4
sub 3 4 3 ; n * Sxx - Sx^2 => loc 3 (to be stored for r calculation)
div 6 3 21 ; coefficient B => loc 21
cpy 1 10 1 ; copy Sx to loc 1
fma 0 21 1 ; B * Sx => loc 1
sub 11 1 1 ; Sy - B * Sx => loc 1
div 1 15 20 ; coefficient A => loc 20
cpy 1 11 2 ; copy Sy to loc 2
fma 0 11 2 ; (Sy) ^ 2 => loc 2
cpy 1 15 5 ; n to loc 5
fma 0 13 5 ; n * Syy => loc 5
sub 5 2 2 ; n * Syy - Sy^2 => loc 2
cpy 1 3 1 ; loc 3 => loc 1
fma 0 2 1 ; loc 3 * loc 2 => loc 1
sqr 0 1 1 ; sqrt(loc 1) => loc 1
div 6 1 22 ; correlation coefficient r => loc 22
out 20 22 0 ; output all three resulting numbers
+77
View File
@@ -0,0 +1,77 @@
; Lunar Lander game in MU8A for mu808 VM
; On each turn, the following parameters are displayed in this order:
; altitude (meters), speed (m/s) and remaining fuel (kg)
; Your goal is to apply (or not apply) thrust
; (values that make any sense are 0 to 2000) every 10 seconds of flight
; and get the lunar module to land safely without running out of fuel.
; At the end, the game shows one of the following statuses:
; 4444 is a disaster landing with no survivors,
; 5555 is a crash landing with the crew surviving the impact,
; 6666 is a hard landing with some damage to the pod,
; 7777 is a good landing,
; 8888 is a perfect landing.
; Created by Luxferre in 2025, released into public domain
set 1 0 50 ; set constant 1 to loc 50
set 10 0 52 ; altitude threshold AND time period into loc 52
set 16 2500 53 ; freefall speed delta into loc 53
set 7480 0 54 ; capsule weight (in kg) into loc 54
set 3 4483 55 ; fuel burn rate (3.4483) into loc 55
set 2900 0 56 ; exhaust velocity (m/s) into loc 56
set 1930 0 40 ; starting altitude (m) into loc 40
set 100 0 57 ; store 100 into loc 57
fma 0 57 40 ; multiply this altitude value by 100
set 1609 0 41 ; starting speed (m/s) into loc 41
set 7260 0 42 ; starting fuel (in kg) into loc 42
set 4444 0 30 ; disaster code into loc 30
set 5555 0 31 ; crash landing code into loc 31
set 6666 0 32 ; damage landing code into loc 32
set 7777 0 33 ; good landing code into loc 33
set 8888 0 34 ; perfect landing code into loc 34
set 26 6667 0 36 ; criterion for crash landing into loc 36
set 9 7300 0 37 ; criterion for damage landing into loc 37
set 4 4445 0 38 ; criterion for good landing into loc 38
set 0 4500 0 39 ; criterion for perfect landing into loc 39
:lp out 40 42 0 ; loop start; print altitude, speed and fuel
set 0 0 2 ; set fuel loss in loc 2 to 0
jmp 4 42 :cnt ; skip prompting for thrust if already out of fuel
inp 1 1 0 ; prompt for thrust into loc 1
cpy 1 1 2 ; copy thrust into loc 2
fma 0 55 2 ; multiply thrust by fuel burn rate to get fuel loss in loc 2
cpy 1 42 3 ; copy fuel weight into loc 3
:cnt fma 54 50 3 ; add capsule weight and fuel weight to get m0 in loc 3
sub 3 2 4 ; subtract m0 and fuel loss value to get m1 in loc 4
div 3 4 1 ; divide m0 by m1 and rewrite the result into loc 1
nel 1 1 1 ; calculate natural logarithm of the previous result
fma 0 56 1 ; multiply the result by the exhaust velocity to get thrust speed delta
sub 41 1 41 ; subtract the thrust speed delta from the current speed
fma 53 50 41 ; add the freefall speed delta to the current speed
cpy 1 41 3 ; copy the speed value into loc 3
fma 0 52 3 ; multiply speed by time period into loc 3
sub 40 3 40 ; decrease the altitude by the result of this operation
sub 42 2 42 ; decrease the amount of fuel by fuel loss value still at loc 2
jmp 1 42 :flc ; skip the next instruction if the amount of fuel is positive
set 0 0 42 ; just set the amount of fuel to zero if it's negative
:flc jmp 1 40 :al ; do the same for altitude
set 0 0 40 ; set it to zero if negative
:al sub 40 52 1 ; subtract the threshold from the altitude
jmp 1 1 :lp ; go to the loop start if the altitude is above the threshold
out 40 42 0 ; print the final altitude/speed/fuel
sub 41 39 1 ; subtract the perfect speed
jmp 1 1 :good ; skip if > 0
out 34 34 0 ; output the perfect score
jmp 6 0 :end ; go to end
:good sub 41 38 1 ; subtract the good speed
jmp 1 1 :dmg ; skip if > 0
out 33 33 0 ; output the good score
jmp 6 0 :end ; go to end
:dmg sub 41 37 1 ; subtract the damage speed
jmp 1 1 :crsh ; skip if > 0
out 32 32 0 ; output the damage score
jmp 6 0 :end ; go to end
:crsh sub 41 36 1 ; subtract the crash speed
jmp 1 1 :disa ; skip if > 0
out 31 31 0 ; output the crash score
jmp 6 0 :end ; go to end
:disa out 30 30 0 ; output the disaster score
:end nop 0 0 0 ; program end label
+68
View File
@@ -0,0 +1,68 @@
; Bulls and Cows game in MU8A for mu808 VM
; Enter your guesses digit by digit, you have 7 attempts
; After each guess, the game replies with bulls.cows
; (if you have 4.0, you win)
; On victory or after running out of attempts, the game
; displays the target digits and halts
; Created by Luxferre in 2025, released into public domain
set 0 0 60 ; store digits 0 to 9 to locations 60 to 69
set 1 0 61
set 2 0 62
set 3 0 63
set 4 0 64
set 5 0 65
set 6 0 66
set 7 0 67
set 8 0 68
set 9 0 69
set 1 0 50 ; store 1 into loc 50
set 4 0 11 ; store the counter to loc 11
set 60 0 70 ; store the source base address 60 to loc 70
set 80 0 71 ; store the target base address 80 to loc 71
set 10 0 3 ; store the constant 10 to loc 3
:dsl set 9 0 1 ; store the upper boundary to loc 1
rnd 0 1 1 ; store a random digit into loc 1
fma 70 50 1 ; add the source base address to loc 1
set 2 0 2 ; init loc 2 with its own address
cpy 2 1 2 ; read the value at that loc 1 address back into loc 2
sub 2 3 4 ; subtract 10 from the read value into loc 4
jmp 0 4 :dsl ; jump back to digit selection if the value at loc 4 is zero
iat 0 3 1 ; enable indirect addressing to...
cpy 0 0 0 ; ...set the value at the address still at loc 1 to 10
cpy 1 11 1 ; copy the counter into loc 1
fma 71 50 1 ; add the counter and the target base address (result is 81..84 at loc 1)
set 2 0 5 ; set the constant 2 into loc 5
cpy 2 5 1
sub 11 50 11 ; decrement the counter at loc 11
jmp 1 11 :dsl ; jump back to digit selection if the counter is over zero
set 0 1000 51 ; store 0.1 to loc 51
set 7 0 11 ; the number to guess is at loc 81..84; store the attempt counter to loc 11
:prm inp 91 94 0 ; prompt the player to enter the number digit by digit into loc 91 to 94
set 90 0 70 ; store the entered digits base address to loc 70
set 0 0 20 ; init bull/cow counter at loc 20
set 4 0 21 ; init outer loop counter at loc 21
:olp set 4 0 22 ; start of the outer loop; init inner loop counter at loc 22
:ilp cpy 1 21 15 ; start of the inner loop; copy the outer counter
fma 71 50 15 ; shape the address of the target digit in loc 15
cpy 1 22 16 ; copy the inner counter
fma 70 50 16 ; shape the address of the entered digit in loc 16
iat 15 16 50 ; prepare to save the digits difference into loc 1 (1 is stored at loc 50)
sub 0 0 0 ; do it
jmp 5 1 :ei ; jump to the next comparator if the digits don't match
sub 21 22 2 ; save the _indices_ difference into loc 2
jmp 5 2 :cc ; jump to the cow counter if the indices don't match
fma 50 50 20 ; increase the bull counter if they do
jmp 6 0 :ei ; skip the next instruction
:cc fma 51 50 20 ; increase the cow counter if they don't
:ei sub 22 50 22 ; decrease the inner loop counter
jmp 1 22 :ilp ; jump to the start of the inner loop if the inner counter is over zero
sub 21 50 21 ; decrease the outer loop counter
jmp 1 21 :olp ; jump to the start of the outer loop if the outer counter is over zero
out 20 20 0 ; output the match result
set 4 0 7 ; store the constant 4.0 at loc 7
sub 20 7 1 ; store the difference between the result and 4 to loc 1
jmp 0 1 :end ; jump to the last instruction if they match
sub 11 50 11 ; decrement the attempt counter at loc 11
jmp 1 11 :prm ; jump to guess prompt if the counter is over zero
:end out 81 84 0 ; output the target number before halting
Executable
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
# mu808 VM reference implementation in Python 3 / MicroPython
# See the documentation in README.md
# Created by Luxferre in 2025, released into public domain
import sys, math, random, re
advterm = False
try:
import tty, termios # for the character input port
advterm = True
except:
pass
MEMLIMIT:int = 16384 # 16K floats for data, 64K integers for program
PMEM = [[0,0,0,0] for i in range(0, MEMLIMIT)] # program memory
DMEM = [0.0 for i in range(0, MEMLIMIT)] # data memory
traceflag:bool = False
runlimit:int = MEMLIMIT # default runlimit is the program space size
runcount:int = 0
# mu808 mnemonics for assembly and disasssembly
mnemos = ['nop', 'jmp', 'iat', 'out', 'inp', 'set', 'cpy', 'fma',
'sub', 'div', 'mdf', 'abs', 'sqr', 'nel', 'tri', 'rnd']
# port output
def portout(port:int, data:float):
if port == 0: # standard value output port
print(data)
elif port == 1: # character output port
sys.stdout.write(chr(int(data)&255))
sys.stdout.flush()
# port input
def portin(port:int):
val = 0
if port == 0: # standard input port
try:
val = float(input())
except ValueError:
val = 0
if port == 2: # character input port
if advterm: # normal OS with termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
else: # crippled OS without termios
ch = sys.stdin.read(1)
val = ord(ch)
return float(val)
# instruction line execution function
# the main mu808 logic is defined here
def ilexec(lno:int, cmd:int=0, x:int=0, y:int=0, z:int=0):
global runcount, PMEM, DMEM
halt = False
while not halt:
if traceflag:
print(f'PC: {lno} INSTR: {cmd} {x} {y} {z}')
DMEM[0] = 0.0 # force the value at 0 to always be 0
data_override = False
# perform a boundary check and prefetch the memory values
bcheck = True
try:
v1 = DMEM[x]
v2 = DMEM[y]
v3 = DMEM[z]
except IndexError:
bcheck = False
if bcheck:
# command switch starts here (commands <=0 and >15 are nops)
if cmd == 1: # JMP: conditional/unconditional, direct/indirect jump
if (v2 == 0 and x == 0) or (v2 > 0 and x == 1) or (v2 < 0 and x == 2) \
or (v2 >= 0 and x == 3) or (v2 <= 0 and x == 4) or (v2 != 0 and x == 5) \
or x == 6:
halt = False
lno = z - 1
elif (v2 == 0 and x == 7) or (v2 > 0 and x == 8) or (v2 < 0 and x == 9) \
or (v2 >= 0 and x == 10) or (v2 <= 0 and x == 11) or (v2 != 0 and x == 12) \
or x == 13:
halt = False
lno = int(v3) - 1
elif cmd == 2: # IAT
data_override = True
elif cmd == 3: # OUT
for i in range(x, y + 1):
portout(z, DMEM[i])
elif cmd == 4: # INP
for i in range(x, y + 1):
DMEM[i] = portin(z)
elif cmd == 5: # SET
DMEM[z] = (x % 10000) + (y % 10000) / 10000.
elif cmd == 6: # CPY
if x == 0:
DMEM[z] = y
elif x == 1:
DMEM[z] = v2
elif int(v3) < MEMLIMIT and int(v2) < MEMLIMIT:
DMEM[int(v3)] = DMEM[int(v2)]
elif cmd == 7: # FMA (v1 + v2 * v3)
try:
DMEM[z] = math.fma(v3, v2, v1)
except:
DMEM[z] = v1 + v2 * v3
elif cmd == 8: # SUB
DMEM[z] = v1 - v2
elif cmd == 9: # DIV
if v2 == 0:
DMEM[z] = 0
else:
DMEM[z] = v1 / v2
elif cmd == 10: # MDF
if v2 == 0:
DMEM[z] = math.floor(v1)
else:
DMEM[z] = float(int(v1) % int(v2))
elif cmd == 11: # ABS
DMEM[z] = math.fabs(v2)
elif cmd == 12: # SQR
DMEM[z] = math.sqrt(math.fabs(v2))
elif cmd == 13: # NEL
if x == 0:
DMEM[z] = math.exp(v2)
else:
if v2 == 0:
DMEM[z] = 0
else:
DMEM[z] = math.log(math.fabs(v2))
elif cmd == 14: # TRI
if x == 0:
DMEM[z] = math.sin(v2)
elif x == 1:
DMEM[z] = math.cos(v2)
else:
DMEM[z] = math.atan(v2)
elif cmd == 15: # RND
DMEM[z] = float(random.randint(int(v1), int(v2)))
# command switch ends here
# increment the program counter
lno += 1
if lno >= MEMLIMIT or lno < 1 or (runlimit > 0 and runcount > runlimit):
if traceflag:
print('Memory limit or runlimit hit, halting...')
halt = True
else: # fetch the next instruction
runcount += 1
if data_override and bcheck:
cmd = PMEM[lno][0]
x = int(v1) % MEMLIMIT
y = int(v2) % MEMLIMIT
z = int(v3) % MEMLIMIT
else:
cmd, x, y, z = tuple(PMEM[lno])
# instruction line entry function
def ilenter(lno:int, cmd:int=0, x:int=0, y:int=0, z:int=0):
global traceflag, runlimit, runcount
if lno > 0: # record the instruction line in memory
PMEM[lno] = [cmd, x, y, z]
elif lno == 0: # immediately execute the instruction line
runcount = 0
ilexec(lno, cmd, x, y, z)
elif lno == -1: # display a range of instructions from cmd to x
for i in range(cmd, x + 1):
print(f'@{i}:\t{mnemos[PMEM[i][0]]}|{PMEM[i][0]}', PMEM[i][1],
PMEM[i][2], PMEM[i][3])
elif lno == -2: # clear a range of data or instructions from x to y
for i in range(x, y + 1):
try:
if cmd == 0:
PMEM[i] = [0,0,0,0]
else:
DMEM[i] = 0.0
except IndexError:
pass
elif lno == -3: # turn on/off tracing
if cmd == 0:
traceflag = False
print('Tracing off')
else:
traceflag = True
print('Tracing on')
elif lno == -4: # set the runlimit to the value of cmd
runlimit = cmd
print('Runlimit set to', runlimit)
elif lno == -5: # exit to the environment
print('Bye!')
sys.exit(0)
# main REPL environment
if __name__ == '__main__':
print(f'mu808 v1 by Luxferre\nPROGMEM: {MEMLIMIT} steps\nDATAMEM: {MEMLIMIT} floats')
vreg = re.compile(r'[^-\d]+')
if len(sys.argv) > 1: # preload the file contents
fc = ''
try:
fd = open(sys.argv[1], 'r')
fc = fd.read()
close(fd)
except:
pass
instr = [] # current instruction cache
for instrpart in vreg.split(fc):
if len(instrpart) > 0:
instr.append(int(instrpart))
if len(instr) == 5: # full instruction registered
ilenter(int(instr[0]), abs(int(instr[1])),
abs(int(instr[2])), abs(int(instr[3])), abs(int(instr[4])))
instr = []
instr = [] # current instruction cache
while True: # main interactive loop
rinput = input('> ')
if len(rinput) > 0:
for v in vreg.split(rinput): # loop over the current input
if len(v) > 0:
instr.append(v)
if len(instr) == 5: # full instruction registered
ilenter(int(instr[0]), abs(int(instr[1])),
abs(int(instr[2])), abs(int(instr[3])), abs(int(instr[4])))
instr = []
Executable
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
# a simple assembler/disassembler for the mu808 VM
# supports both text and binary program formats
# usage: mu808asm [at|ab|dt|db|t2b|b2t] file outfile
# modes:
# at: assemble to plaintext (MU8) machine code
# ab: assemble to binary (MU8B) machine code
# dt: disassemble plaintext (MU8) machine code
# db: disassemble binary (MU8B) machine code
# t2b: convert from plaintext MU8 to binary MU8B
# b2t: convert from binary MU8B to plaintext MU8
import sys, re, struct
# mu808 mnemonics for assembly and disasssembly
mnemos = ['nop', 'jmp', 'iat', 'out', 'inp', 'set', 'cpy', 'fma',
'sub', 'div', 'mdf', 'abs', 'sqr', 'nel', 'tri', 'rnd']
# converts the MU8A source code to the plaintext machine code representation
def assemble(source):
labels = {}
out = ''
lno = 0
vreg = re.compile(r'\s+')
for line in source.split('\n'):
fields = []
line = line.split(';')[0].strip()
if len(line) > 0: # actual line to be counted on
lno += 1 # populate the line number
fields = vreg.split(line)[:5] # split it into fields
if fields[0].startswith(':'): # this is a label
labels[fields[0]] = lno # remember the label
fields = fields[1:] # remove the label from fields
try:
cmd = mnemos.index(fields[0].lower())
except ValueError:
try:
cmd = int(cmd)
except:
print('Assembly error - unknown mnemonic at line', lno)
sys.exit(1)
try:
out += ' '.join([str(lno), str(cmd), fields[1], fields[2], fields[3]]) + '\n'
except IndexError:
print('Assembly error - not enough operands at line', lno)
sys.exit(1)
for lbl, lineno in labels.items():
out = out.replace(lbl, str(lineno))
return out
# converts the plaintext machine code representation to MU8A source code
def disassemble(machcode):
vreg = re.compile(r'\s+')
iindex = 0
instr = []
instrs = []
for instrpart in vreg.split(machcode):
if iindex == 5:
instrs.append(instr)
instr = []
iindex = 0
if len(instrpart) > 0:
instr.append(int(instrpart))
iindex += 1
# now, instrs contains 5-number groups
assembly = [None for i in range(0, 16384)]
labels = {}
for instr in instrs: # iterate over each instruction
lno, cmd, x, y, z = instr
try:
mnemo = mnemos[cmd]
except IndexError:
print('Disassembly error - unknown opcode at line', lno)
sys.exit(1)
if cmd == 1: # save the label for jump instructions
labels[z] = ':lbl_' + str(z)
assembly[lno] = [mnemo, str(x), str(y), labels[z]]
else:
assembly[lno] = [mnemo, str(x), str(y), str(z)]
out = ''
lno = 0
for asline in assembly:
if lno in labels:
out += labels[lno] + ' '
if asline is not None:
out += ' '.join(asline) + '\n'
lno += 1
return out
# converts the plaintext machine code representation to binary representation
def tobinary(txtrep):
instrs = [] # instruction list to store here
vreg = re.compile(r'\s+')
iindex = 0
instr = []
for instrpart in vreg.split(txtrep):
if iindex == 5:
instrs.append(instr)
instr = []
iindex = 0
if len(instrpart) > 0:
instr.append(int(instrpart))
iindex += 1
# now, instrs contains 5-number groups
out = b''
# the format is: cmd (8 bits), lno (14 bits), arg1 to arg3 (14 bits each)
for instr in instrs: # iterate over each instruction
b1 = instr[1] & 15
b2 = (instr[0] >> 6) & 255
b3 = ((instr[0] << 2) | (instr[2] >> 12)) & 255
b4 = (instr[2] >> 4) & 255
b5 = ((instr[2] << 4) | (instr[3] >> 10)) & 255
b6 = (instr[3] >> 2) & 255
b7 = ((instr[3] << 6) | (instr[4] >> 8)) & 255
b8 = instr[4] & 255
out += struct.pack('BBBBBBBB', b1, b2, b3, b4, b5, b6, b7, b8)
return out
# converts the binary machine code representation to plaintext representation
def totext(binrep):
out = ''
while len(binrep) > 0:
chunk = binrep[0:8]
binrep = binrep[8:]
b1, b2, b3, b4, b5, b6, b7, b8 = struct.unpack('BBBBBBBB', chunk)
cmd = b1 & 15
lno = ((b2 << 6) | (b3 >> 2)) & 16383
x = ((b3 << 12) | (b4 << 4) | (b5 >> 4)) & 16383
y = ((b5 << 10) | (b6 << 2) | (b7 >> 6)) & 16383
z = ((b7 << 8) | b8) & 16383
out += ' '.join([str(lno), str(cmd), str(x), str(y), str(z)]) + '\n'
return out
def main():
if len(sys.argv) < 4:
print("Usage: mu808asm [at|ab|dt|db|t2b|b2t] file outfile")
return
mode = sys.argv[1]
srcfile = sys.argv[2]
targetfile = sys.argv[3]
srctext = ''
inpmode = 'r'
if mode == 'b2t' or mode == 'db':
inpmode = 'rb'
try:
fd = open(srcfile, inpmode)
srctext = fd.read()
close(fd)
except:
pass
if len(srctext) > 0:
outmode = 'w'
output = ''
if mode == 'b2t': # binary-to-text
output = totext(srctext)
elif mode == 't2b': # text-to-binary
output = tobinary(srctext)
outmode = 'wb'
elif mode == 'dt': # disassemble text
output = disassemble(srctext)
elif mode == 'db': # disassemble binary
srctext = totext(srctext)
output = disassemble(srctext)
else: # assemble
output = assemble(srctext) # default mode
if mode == 'ab':
output = tobinary(output)
outmode = 'wb'
# write the output file
try:
fd = open(targetfile, outmode)
fd.write(output)
close(fd)
except:
pass
else:
print('Nothing to process!')
if __name__ == '__main__':
main()