mixture
provides a robust simulation environment for MIX computers
used extensively in The Art of Computer Programming series written
by D. E. Knuth.
This crate sports:
- MIX simulation via [
MixVM
] - I/O device simulation via [
IODevice
] (enabled byio
feature) #[no_std]
compatibility
std
- Enablestd
support.io
- Enable I/O module of MIX.
x-ieee754
- Enable IEEE 754-compatible floating-point extension.x-binary
- Enable binary operation extension (TAOCP Section 4.5.2).
All features are enabled by default.
This example is a simple program that writes a hello message to stdout
. It demonstrates basic
workflow of creating, initializing, adding I/O device to, and running a MIX virtual machine
with mixture
.
use mixture::{ErrorCode, FullWord, IODevice, Instruction, MixVM, Opcode};
// Define common constants
const ADDR_TEXT_HELLO: i16 = 2000;
const DEV_PRINTER: u8 = 18;
// Define a line printer I/O device; this is not necessary: results can be read
// from VM memory directly, but using an I/O device is more illustrative.
struct LinePrinter {}
impl LinePrinter {
const BLOCK_SIZE: usize = 3;
}
impl IODevice for LinePrinter {
fn read(&mut self, _: &mut [FullWord]) -> Result<(), ()> {
// Our device is a printer, so it never reads.
Err(())
}
fn write(&mut self, data: &[FullWord]) -> Result<(), usize> {
if data.len() != Self::BLOCK_SIZE {
// Show that we have not printed anything before we bail out.
return Err(0);
}
for i in 0..Self::BLOCK_SIZE {
let bytes = &data[i][1..=5]; // Ignore sign byte.
for &b in bytes {
print!("{}", char::from_u32(b as u32).unwrap_or('?'));
}
}
Ok(())
}
fn control(&mut self, _: i16) -> Result<(), ()> {
// Our device has no special commands defined.
Ok(())
}
fn is_busy(&self) -> Result<bool, ()> {
Ok(false)
}
fn is_ready(&self) -> Result<bool, ()> {
Ok(true)
}
fn get_block_size(&self) -> usize {
Self::BLOCK_SIZE
}
}
fn main() {
// Create a VM
let mut mix = MixVM::new();
mix.reset();
// Fill in some instructions
// 0 OUT ADDR_TEXT_HELLO(DEV_PRINTER)
// 1 HLT 0
mix.mem[0] = Instruction::new(ADDR_TEXT_HELLO, DEV_PRINTER, 0, Opcode::Out).into();
mix.mem[1] = Instruction::new(0, 2, 0, Opcode::Special).into();
// Fill in constants
mix.mem[ADDR_TEXT_HELLO as u16] =
FullWord::from_bytes([FullWord::POS, b'H', b'E', b'L', b'L', b'O']);
mix.mem[ADDR_TEXT_HELLO as u16 + 1] =
FullWord::from_bytes([FullWord::POS, b' ', b'W', b'O', b'R', b'L']);
mix.mem[ADDR_TEXT_HELLO as u16 + 2] =
FullWord::from_bytes([FullWord::POS, b'D', b'!', b' ', b'\r', b'\n']);
// Attach device
mix.io_devices[DEV_PRINTER as usize] = Some(Box::new(LinePrinter {}));
// Start the VM
mix.restart();
// Run main loop
let error_code = loop {
let result = mix.step();
match result {
Ok(()) => continue,
Err(c) => break c,
}
};
// Check if the VM has shut down gracefully.
// Now you should see "HELLO WORLD! " with line break in stdout.
assert_eq!(error_code, ErrorCode::Halted);
}
MIX is the world's first polyunsaturated computer. Like most machines, it has an identifying number - the 1009. This number was found by taking 16 actual computers very similar to MIX and on which MIX could easily be simulated, then averaging their numbers with equal weight:
⌊(360 + 650 + 709 + 7070 + U3 + SS80 + 1107 + 1604 + G20 + B220 + S2000 + 920 + 601 + H800 + PDP-4 + II)⌋ / 16 = 1009.
The same number may also be obtained in a simpler way by taking Roman numerals.
D. E. Knuth, The Art of Computer Programming (Volume 1, 3rd. ed.)
The MIX computers are a model computer architecture, including hardware abstractions and instruction set architecture (ISA), proposed by D. E. Knuth. Bearing many features from the 1960s and 1970s, it is now considered obsolete, but it still provides adequate context for ISA development.
The basic unit of memory in MIX is a byte. Note that in MIX, each byte could contain arbitrary amount of information. MIX only places a lower limit on byte size: A byte must be able to hold at least 64 distinct values. A sound algorithm implementation in MIX should work properly regardless of how big a byte is.
Note
Specific to mixture
: The byte size in mixture
implementation is 256.
Two adjacent bytes can express the numbers 0 through 4,095.
Three adjacent bytes can express the numbers 0 through 262,143.
Four adjacent bytes can express the numbers 0 through 16,777,215.
Five adjacent bytes can express the numbers 0 through 1,073,741,823.
A computer word consists of five bytes and a sign. The sign portion has only two
possible values, +
and -
.
Note
Specific to mixture
: The sign in mixture
is also encoded in a byte, whose
only valid content is [Word<N, P>::POS
] and [Word<N, P>::NEG
]. Other
values written into the sign byte are considered undefined.
There are nine registers in MIX.
A
-register (Accumulator) consists of five bytes and a sign, used in most operations as operand.X
-register (Extension), likewise, comprises five bytes and a sign, used in some operations as an extension toA
-register at more significant digits.I
-registers (Index registers)I1
,I2
,I3
,I4
,I5
, andI6
each hold two bytes together with a sign, used often as indices while counting and addressing.J
-register (Jump address) holds two bytes; it behaves as if its sign is always+
, used as return address while performing jumps.
Each register is denoted with a prefix 'r
' added to its name, e.g. 'rA
' for 'register
A
'.
Note
Specific to mixture
: rA
and rX
have a type of [FullWord
]. rI1-6
have a type of [HalfWord
]. rJ
has a type of [PosHalfWord
]. All three types
are aliases for different instantiations of [Word<N, P>
].
MIX has some more states that are not registers, and could only be manipulated through specific instructions:
- An overflow toggle (flag for overflow);
- A comparison indicator (having three values:
LESS
,EQUAL
orGREATER
); - A memory (4000 words of storage, each containing five bytes and a sign);
- input-output devices (cards, tapes, disks, etc.).
Note
Specific to mixture
: There are several implementation-specific details of the above
states.
- The overflow toggle is a private field that is not exposed to users.
- The comparison indicator could contain values in the enum [
CompIndicator
]. - The memory area is a struct named [
Mem
]. - I/O devices are only available while crate feature
io
is enabled, and are described by dynamic trait [IODevice
].
The bytes and sign are indexed as follows:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
± | Byte | Byte | Byte | Byte | Byte |
MIX programmers are allowed to use only part of a word in most instructions. In such cases,
a field specification could be given to denote part of a word. Given a range (L:R)
, it
interprets bytes from L
to R
(both inclusive) as a whole. Examples of field specifications
are:
(0:0)
, the sign only.(0:2)
, the sign and the first two bytes.(0:5)
, the whole word; this is the most common field specification.(1:5)
, the whole word except for the sign.(4:4)
, the fourth byte only.(4:5)
, the two least significant bytes.
Note
Specific to mixture
: [Word
][Word<N, P>]s are able to be indexed by both [usize
]
scalars and [core::ops::RangeInclusive<usize>
] ranges, so as to match the semantics proposed
in the book.
The use of field specification varies among instructions. When encoded in an instruction,
field specification is packed into one-byte scalar equals to 8 * L + R
.
Note
Specific to mixture
: There is a trait named [ToRangeInclusive<T>
] to simplify
this conversion process. It is already implemented for [u8
] by default.
Each instruction in MIX could fit in a single word. The encoding of instructions is as follows:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
± | A |
A |
I |
F |
C |
C
: the operation code specifying what operation is to be performed. For example,C = 8
specifies the operation [LDA
][Opcode::LdA], "load the register A".
Note
Specific to mixture
: See [Opcode
] for a complete list of supported opcodes.
F
: the modification or field of an instruction. It is usually a packed field specification said above. For example, ifC = 8
andF = 11
, the operation would be "load the register A with(1:3)
field." SometimesF
is used to specify additional parameters, as it is on I/O instructions; in other cases it further refines the operation to carry out, as on [register modification][Opcode::ModifyA] instruction series.I
: the index part of an instruction. It must be an integer in the range between 0 and 6 (both inclusive).A
: the address of the instruction. Note that the sign is part of the address.
The use of I
and A
will be discussed in the Addressing section.
Memory addressing in MIX utilizes the A
and I
fields in every instruction. During
instruction decoding, the target memory location M
is produced using these two fields. M
refers to some memory location at most times, and in this sense, it must be in the range of
0 to 3999 due to the number of memory cells in MIX. The notation CONTENT(M)
is used in TAOCP
to denote the content of memory location M
.
If I = 0
, then direct addressing is used, which means M
is set to A
without change.
Otherwise, if I = i
(where i
is an integer between 1 and 6), the content of index register
rIi
is added algebraically to A
to produce M
, resembling a base-offset addressing mode.
Note
Specific to mixture
: If I
is out of valid range, the VM will halt, and
[ErrorCode::InvalidIndex
] is returned as error.
If the produced M
does not fit in two bytes, the result is undefined.
Note
Specific to mixture
: mixture
uses [u16
] for addresses, and will (depending on build
flags) panic or silently wrap should any numerical overflow or underflow happens.
For a brief sketch of the operators, see the inline documentation of variants in [Opcode
].
For a detailed explanation of instruction semantics and effects, please refer to The Art of Computer Programming (Volume 1, 3rd. ed.) by D. E. Knuth, or visit Esolang/MIX_(Knuth).
Note
Specific to mixture
: This section decribes features exclusive to mixture
and/or implemented in a non-TAOCP way.
This extension adds support for binary32
floating-point format as described in
IEEE 754-2008 and Rust [f32
].
A binary32
scalar fully occupies a [FullWord
]. When loading binary32
scalars
in MIX, the only valid field specification is (0:5)
. Loading only a part of a
[FullWord
], or manipulating any byte in a binary32
scalar with instructions
not prefixed with F32
will yield undefined result.
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
± | X | f |
f |
f |
f |
In reality, only (2:5)
part of a [FullWord
] is used to store a binary32
scalar.
The sign byte is only set while storing computation results; its value is not honored when reading
computation operands. Thus, such a wrongly-signed word representing the quantity +1.0f32
will
never be constructed by mixture
:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
- | X | 0x3F |
0x80 |
0x00 |
0x00 |
but can be created by hand. If such a quantity is used in a calculation, its actual value will be
+1.0f32
, rather than -1.0f32
.
F32CVTF322I4B
([Opcode::Special
],F = 3
): Convert and round IEEE 754binary32
to 4-bytes integer.F32CVTF322I2B
([Opcode::Special
],F = 4
): Convert and round IEEE 754binary32
to 2-bytes integer.F32CVTF322I1B
([Opcode::Special
],F = 5
): Convert and round IEEE 754binary32
to 1-byte integer.F32CVTI4B2F32
([Opcode::Special
],F = 6
): Convert 4-bytes integer to IEEE 754binary32
.F32CVTI2B2F32
([Opcode::Special
],F = 7
): Convert 2-bytes integer to IEEE 754binary32
.F32CVTI1B2F32
([Opcode::Special
],F = 8
): Convert 1-byte integer to IEEE 754binary32
.
These instructions convert source scalars stored in rA
into destination type, and store the
result in rA(0:5)
.
For integer-to-binary32
, the source is fetched from least significant byte and is always
considered positive. For example, if rA
contains:
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
- | 9 |
8 |
7 |
6 |
5 |
performing F32CVTI1B2F32
will yield binary32
representation of +5
in rA(0:5)
.
0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
+ | X | 0x40 |
0xA0 |
0x00 |
0x00 |
When the source scalar is outside the destination type's representable range, the overflow toggle be turned on and source will be clamped to destination type.
Converting a NaN to integer will result in overflow toggle being turned on. The result will be zero.
F32ADD
([Opcode::Add
],F = 7
): IEEE 754binary32
addition.F32SUB
([Opcode::Sub
],F = 7
): IEEE 754binary32
subtraction.F32MUL
([Opcode::Mul
],F = 7
): IEEE 754binary32
multiplication.F32DIV
([Opcode::Div
],F = 7
): IEEE 754binary32
division.
These instrutions takes rA
as left operand and V
as right operand. Result is stored in rA
.
If the calculation result is either NaN or ±Infinity, overflow toggle will be turned on. The result
will still be stored as-is in rA
.
F32CMPA
([Opcode::CmpA
],F = 7
): ComparerA
withV
asbinary32
values.F32CMPX
([Opcode::CmpX
],F = 7
): ComparerX
withV
asbinary32
values.
Compare rA
or rX
against V
as binary32
values. Stores result in comparison indicator.
Note that NaNs are unordered against any other values.
Trivial.
The regular [Opcode::Jmp
] variants also works for binary32
comparisons.
F32JORD
([Opcode::Jmp
],F = 11
): Jump on ordered.F32JUNORD
([Opcode::Jmp
],F = 12
): Jump on unordered.
Perform jump according to last binary32
comparison result.
Trivial.
This extension adds support for binary operations to registers and words, allowing for bit-wise shifts and conditional jumps on final bit of registers (even/odd).
SLB
([Opcode::Shift
],F = 6
): Shift leftrAX
binary. The contents ofrA
andrX
are shifted leftM
binary places. The signs ofrA
andrX
are not affected.)SRB
([Opcode::Shift
],F = 7
): Shift rightrAX
binary.C = 6
;F = 7
. The contents ofrA
andrX
are shifted rightM
binary places.
JAE
([Opcode::JA
],F = 6
): JumprA
even.JAO
([Opcode::JA
],F = 7
): JumprA
odd.JXE
([Opcode::JX
],F = 6
): JumprX
even.JXO
([Opcode::JX
],F = 7
): JumprX
odd.
These instructions look at the least significant bit in rA
and rX
, performing jumps
according to its oddity.
This extension adds support for binary arithmetics, resembling the classical 'bit operations' in
various programming languages. These operations are indeed not authentic MIX (which requires
operations to be independent of byte size and numerical base), so they are categorized under
[Opcode::Special
] sections.
NOT
([Opcode::Special
],F = 9
): Perform bitwise NOT onrA
, then store result inrA
.AND
([Opcode::Special
],F = 10
): Perform bitwise AND onV
andrA
, then store result inrA
.OR
([Opcode::Special
],F = 11
): Perform bitwise OR onV
andrA
, then store result inrA
.XOR
([Opcode::Special
],F = 12
): Perform bitwise XOR onV
andrA
, then store result inrA
.
In every operations listed in this extension, [Word<N, P>::POS
] is treated as 0x0
, while
[Word<N, P>::NEG
] is treated as 0x1
.