From 6bc0a916ebfb96f2d5123a51942c6d42a1d42a0c Mon Sep 17 00:00:00 2001 From: Luxferre Date: Tue, 22 Apr 2025 20:02:27 +0300 Subject: [PATCH] initial upload --- README.md | 339 +++++++++++++++++++++++++++++++++++++++++ examples/compound.mu8a | 13 ++ examples/hellorld.mu8a | 19 +++ examples/linreg.mu8a | 55 +++++++ examples/lunar.mu8a | 77 ++++++++++ examples/moo.mu8a | 68 +++++++++ mu808.py | 225 +++++++++++++++++++++++++++ mu808asm.py | 181 ++++++++++++++++++++++ 8 files changed, 977 insertions(+) create mode 100644 README.md create mode 100644 examples/compound.mu8a create mode 100644 examples/hellorld.mu8a create mode 100644 examples/linreg.mu8a create mode 100644 examples/lunar.mu8a create mode 100644 examples/moo.mu8a create mode 100755 mu808.py create mode 100755 mu808asm.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..094a190 --- /dev/null +++ b/README.md @@ -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. diff --git a/examples/compound.mu8a b/examples/compound.mu8a new file mode 100644 index 0000000..2aa4536 --- /dev/null +++ b/examples/compound.mu8a @@ -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 diff --git a/examples/hellorld.mu8a b/examples/hellorld.mu8a new file mode 100644 index 0000000..3c22ea5 --- /dev/null +++ b/examples/hellorld.mu8a @@ -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 diff --git a/examples/linreg.mu8a b/examples/linreg.mu8a new file mode 100644 index 0000000..cb4f4c1 --- /dev/null +++ b/examples/linreg.mu8a @@ -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 diff --git a/examples/lunar.mu8a b/examples/lunar.mu8a new file mode 100644 index 0000000..14a1ee7 --- /dev/null +++ b/examples/lunar.mu8a @@ -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 diff --git a/examples/moo.mu8a b/examples/moo.mu8a new file mode 100644 index 0000000..121bb98 --- /dev/null +++ b/examples/moo.mu8a @@ -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 diff --git a/mu808.py b/mu808.py new file mode 100755 index 0000000..5a0b62d --- /dev/null +++ b/mu808.py @@ -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 = [] + diff --git a/mu808asm.py b/mu808asm.py new file mode 100755 index 0000000..f63f28f --- /dev/null +++ b/mu808asm.py @@ -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()