lc3_ensemble/sim/mem.rs
1//! Memory handling for the LC-3 simulator.
2//!
3//! This module consists of:
4//! - [`Word`]: A mutable memory location.
5//! - [`MemArray`]: The memory array.
6//! - [`RegFile`]: The register file.
7
8use rand::rngs::StdRng;
9use rand::Rng;
10
11use crate::ast::Reg;
12
13/// A memory location that can be read and written to.
14///
15/// # Reading
16///
17/// A word's value can be read with:
18/// - [`Word::get`] to directly access the value, ignoring any initialization state
19/// - [`Word::get_if_init`] to directly access the value after verifying initialization state
20///
21/// See the respective functions for more details.
22///
23/// Both functions return the unsigned representation of the word.
24/// If needed, this can be converted to a signed integer with typical `as` casting (`data as i16`).
25///
26/// # Writing
27///
28/// A word can be written into with a value or with another word:
29/// - [`Word::set`] to read a value into this word
30/// - [`Word::set_if_init`] to read a word into this word
31///
32/// [`Word::set_if_init`] may be more useful in situations where initialization state needs to be preserved
33/// or when it needs to be verified.
34///
35/// See the respective functions for more details.
36///
37/// Words can also be written to by applying assign operations (e.g., add, sub, and, etc.).
38/// All arithmetic operations that can be applied to words are assumed to be wrapping.
39/// See those implementations for more details.
40///
41/// # Initialization
42///
43/// Internally, each memory location keeps track of two fields:
44/// 1. its data (i.e., the value stored at this location)
45/// 2. which bits of its data are truly "initialized" (as in the program knows what values are present there)
46///
47/// This second field is not used except for when the simulator is set to strict mode.
48/// Then, this second field is leveraged to detect if uninitialized memory is being
49/// written to places it shouldn't be (e.g., PC, addresses, registers and memory).
50///
51/// When a `Word` is created for memory/register files (i.e., via [`Word::new_uninit`]),
52/// it is created with the initialization bits set to fully uninitialized.
53/// The data associated with this `Word` is decided by the creation strategy
54/// (see [`super::MachineInitStrategy`] for details).
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct Word {
57 data: u16,
58 init: u16
59}
60
61const NO_BITS: u16 = 0;
62const ALL_BITS: u16 = 1u16.wrapping_neg();
63
64impl Word {
65 /// Creates a new word that is considered uninitialized.
66 pub fn new_uninit<F: WordFiller + ?Sized>(fill: &mut F) -> Self {
67 Self {
68 data: fill.generate(),
69 init: NO_BITS,
70 }
71 }
72 /// Creates a new word that is initialized with a given data value.
73 pub fn new_init(data: u16) -> Self {
74 Self {
75 data,
76 init: ALL_BITS,
77 }
78 }
79
80 /// Reads the word, returning its unsigned representation.
81 ///
82 /// The data is returned without checking for initialization state.
83 /// If the initialization state should be checked before trying to query the data,
84 /// then [`Word::get_if_init`] should be used instead.
85 pub fn get(&self) -> u16 {
86 self.data
87 }
88 /// Reads the word if it is properly initialized under strictness requirements, returning its unsigned representation.
89 ///
90 /// This function is more cognizant of word initialization than [`Word::get`].
91 /// - In non-strict mode (`strict == false`), this function unconditionally allows access to the data regardless of initialization state.
92 /// - In strict mode (`strict == true`), this function verifies `self` is fully initialized, raising the provided error if not.
93 pub fn get_if_init<E>(&self, strict: bool, err: E) -> Result<u16, E> {
94 match !strict || self.is_init() {
95 true => Ok(self.data),
96 false => Err(err)
97 }
98 }
99
100 /// Writes to the word.
101 ///
102 /// This sets the word to the `data` value assuming it is **fully** initialized
103 /// and correspondingly sets the initialization state to be fully initialized.
104 ///
105 /// If the initialization state of the `data` value should be checked before
106 /// trying to write to the word, then [`Word::set_if_init`] should be used instead.
107 pub fn set(&mut self, data: u16) {
108 self.data = data;
109 self.init = ALL_BITS;
110 }
111 /// Writes to the word while verifying the data stored is properly initialized under strictness requirements.
112 ///
113 /// This function is more cognizant of word initialization than [`Word::set`].
114 /// - In non-strict mode, this function preserves the initialization data of the `data` argument.
115 /// - In strict mode, this function verifies `data` is fully initialized, raising the provided error if not.
116 pub fn set_if_init<E>(&mut self, data: Word, strict: bool, err: E) -> Result<(), E> {
117 match !strict || data.is_init() {
118 true => {
119 *self = data;
120 Ok(())
121 },
122 false => Err(err)
123 }
124 }
125
126 /// Checks that a word is fully initialized
127 pub fn is_init(&self) -> bool {
128 self.init == ALL_BITS
129 }
130 /// Clears initialization of this word.
131 pub fn clear_init(&mut self) {
132 self.init = NO_BITS;
133 }
134}
135impl From<u16> for Word {
136 /// Creates a fully initialized word.
137 fn from(value: u16) -> Self {
138 Word::new_init(value)
139 }
140}
141impl From<i16> for Word {
142 /// Creates a fully initialized word.
143 fn from(value: i16) -> Self {
144 Word::new_init(value as u16)
145 }
146}
147
148impl std::ops::Not for Word {
149 type Output = Word;
150
151 /// Inverts the data on this word, preserving any initialization state.
152 fn not(self) -> Self::Output {
153 // Initialization state should stay the same after this.
154 let Self { data, init } = self;
155 Self { data: !data, init }
156 }
157}
158
159
160impl std::ops::Add for Word {
161 type Output = Word;
162
163 /// Adds two words together (wrapping if overflow occurs).
164 ///
165 /// If the two words are fully initialized,
166 /// the resulting word will also be fully initialized.
167 /// Otherwise, the resulting word is fully uninitialized.
168 fn add(self, rhs: Self) -> Self::Output {
169 let Self { data: ldata, init: linit } = self;
170 let Self { data: rdata, init: rinit } = rhs;
171
172 if rdata == 0 && rinit == ALL_BITS { return self; }
173 if ldata == 0 && linit == ALL_BITS { return rhs; }
174
175 let data = ldata.wrapping_add(rdata);
176
177 // Close enough calculation:
178 // If both are fully init, consider this word fully init.
179 // Otherwise, consider it fully uninit.
180 let init = match linit == ALL_BITS && rinit == ALL_BITS {
181 true => ALL_BITS,
182 false => NO_BITS,
183 };
184
185 Self { data, init }
186 }
187}
188impl std::ops::AddAssign for Word {
189 fn add_assign(&mut self, rhs: Self) {
190 *self = *self + rhs;
191 }
192}
193impl std::ops::AddAssign<u16> for Word {
194 /// Increments the word by the provided value.
195 ///
196 /// If the word was fully initialized,
197 /// its updated value is also fully initialized.
198 /// Otherwise, the resulting word is fully uninitialized.
199 fn add_assign(&mut self, rhs: u16) {
200 *self = *self + Word::from(rhs);
201 }
202}
203impl std::ops::AddAssign<i16> for Word {
204 /// Increments the word by the provided value.
205 ///
206 /// If the word was fully initialized,
207 /// its updated value is also fully initialized.
208 /// Otherwise, the resulting word is fully uninitialized.
209 fn add_assign(&mut self, rhs: i16) {
210 *self = *self + Word::from(rhs);
211 }
212}
213
214
215impl std::ops::Sub for Word {
216 type Output = Word;
217
218 /// Subtracts two words together (wrapping if overflow occurs).
219 ///
220 /// If the two words are fully initialized,
221 /// the resulting word will also be fully initialized.
222 /// Otherwise, the resulting word is fully uninitialized.
223 fn sub(self, rhs: Self) -> Self::Output {
224 let Self { data: ldata, init: linit } = self;
225 let Self { data: rdata, init: rinit } = rhs;
226
227 // This is (self - 0) == self.
228 if rdata == 0 && rinit == ALL_BITS { return self; }
229
230 let data = ldata.wrapping_sub(rdata);
231 // Very lazy initialization scheme.
232 // If both are fully init, consider this word fully init.
233 // Otherwise, consider it fully uninit.
234 let init = match linit == ALL_BITS && rinit == ALL_BITS {
235 true => ALL_BITS,
236 false => NO_BITS,
237 };
238
239 Self { data, init }
240 }
241}
242impl std::ops::SubAssign for Word {
243 fn sub_assign(&mut self, rhs: Self) {
244 *self = *self - rhs;
245 }
246}
247impl std::ops::SubAssign<u16> for Word {
248 /// Decrements the word by the provided value.
249 ///
250 /// If the word was fully initialized,
251 /// its updated value is also fully initialized.
252 /// Otherwise, the resulting word is fully uninitialized.
253 fn sub_assign(&mut self, rhs: u16) {
254 *self = *self - Word::new_init(rhs);
255 }
256}
257impl std::ops::SubAssign<i16> for Word {
258 /// Decrements the word by the provided value.
259 ///
260 /// If the word was fully initialized,
261 /// its updated value is also fully initialized.
262 /// Otherwise, the resulting word is fully uninitialized.
263 fn sub_assign(&mut self, rhs: i16) {
264 *self = *self - Word::new_init(rhs as _);
265 }
266}
267
268
269impl std::ops::BitAnd for Word {
270 type Output = Word;
271
272 /// Applies a bitwise AND across two words.
273 ///
274 /// This will also compute the correct initialization
275 /// for the resulting word, taking into account bit clearing.
276 fn bitand(self, rhs: Self) -> Self::Output {
277 let Self { data: ldata, init: linit } = self;
278 let Self { data: rdata, init: rinit } = rhs;
279
280 let data = ldata & rdata;
281 // A given bit of the result is init if:
282 // - both the lhs and rhs bits are init
283 // - either of the bits are data: 0, init: 1
284 let init = (linit & rinit) | (!ldata & linit) | (!rdata & rinit);
285
286 Self { data, init }
287 }
288}
289impl std::ops::BitAndAssign for Word {
290 fn bitand_assign(&mut self, rhs: Self) {
291 *self = *self & rhs;
292 }
293}
294
295/// Trait that describes types that can be used to create the data for an uninitialized [`Word`].
296///
297/// This is used with [`Word::new_uninit`] to create uninitialized Words.
298pub trait WordFiller {
299 /// Generate a word of data.
300 fn generate(&mut self) -> u16;
301
302 /// Generates an array of [`Word`]s.
303 fn generate_array<const N: usize>(&mut self) -> [Word; N] {
304 std::array::from_fn(|_| Word::new_uninit(self))
305 }
306 /// Generates a heap-allocated array of [`Word`]s.
307 fn generate_boxed_array<const N: usize>(&mut self) -> Box<[Word; N]> {
308 std::iter::repeat_with(|| Word::new_uninit(self))
309 .take(N)
310 .collect::<Box<_>>()
311 .try_into()
312 .unwrap_or_else(|_| unreachable!("iterator should have had {N} elements"))
313 }
314}
315impl WordFiller for () {
316 /// This creates unseeded, non-deterministic values.
317 fn generate(&mut self) -> u16 {
318 rand::random()
319 }
320}
321impl WordFiller for u16 {
322 /// Sets each word to the given value.
323 fn generate(&mut self) -> u16 {
324 *self
325 }
326}
327impl WordFiller for StdRng {
328 /// This creates values from the standard random number generator.
329 ///
330 /// This can be used to create deterministic, seeded values.
331 fn generate(&mut self) -> u16 {
332 self.gen()
333 }
334}
335/// Strategy used to initialize the `reg_file` and `mem` of the [`Simulator`].
336///
337/// These are used to set the initial state of the memory and registers,
338/// which will be treated as uninitialized until they are properly initialized
339/// by program code.
340///
341/// [`Simulator`]: super::Simulator
342#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
343pub enum MachineInitStrategy {
344 /// Initializes each word randomly and non-deterministically.
345 #[default]
346 Unseeded,
347
348 /// Initializes each word randomly and deterministically.
349 Seeded {
350 /// The seed the RNG was initialized with.
351 seed: u64
352 },
353
354 /// Initializes each word to a known value.
355 Known {
356 /// The value to initialize each value to.
357 value: u16
358 }
359}
360
361impl MachineInitStrategy {
362 pub(super) fn generator(&self) -> impl WordFiller {
363 use rand::SeedableRng;
364
365 match self {
366 MachineInitStrategy::Unseeded => WCGenerator::Unseeded,
367 MachineInitStrategy::Seeded { seed } => WCGenerator::Seeded(Box::new(StdRng::seed_from_u64(*seed))),
368 MachineInitStrategy::Known { value } => WCGenerator::Known(*value),
369 }
370 }
371}
372
373enum WCGenerator {
374 Unseeded,
375 Seeded(Box<rand::rngs::StdRng>),
376 Known(u16)
377}
378impl WordFiller for WCGenerator {
379 fn generate(&mut self) -> u16 {
380 match self {
381 WCGenerator::Unseeded => ().generate(),
382 WCGenerator::Seeded(r) => r.generate(),
383 WCGenerator::Known(k) => k.generate(),
384 }
385 }
386}
387
388/// Memory array.
389///
390/// This can be addressed with any `u16` (16-bit address).
391///
392/// This memory array *does* expose memory locations 0xFE00-0xFFFF,
393/// however they are not accessible through normal Simulator operation
394/// (i.e., via [`Simulator::read_mem`]) and [`Simulator::write_mem`].
395///
396/// They can be read and edited via the typical Index traits.
397/// If you wish to see the handling of memory-mapped IO, see the above
398/// [`Simulator`] methods.
399///
400/// [`Simulator`]: super::Simulator
401/// [`Simulator::read_mem`]: super::Simulator::read_mem
402/// [`Simulator::write_mem`]: super::Simulator::write_mem
403/// [`Simulator::default_mem_ctx`]: super::Simulator::default_mem_ctx
404#[derive(Debug)]
405pub struct MemArray(Box<[Word; 1 << 16]>);
406impl MemArray {
407 /// Creates a new memory with a provided word creation strategy.
408 pub fn new(filler: &mut impl WordFiller) -> Self {
409 Self(filler.generate_boxed_array())
410 }
411
412 /// Copies an object file block into this memory.
413 pub(super) fn copy_obj_block(&mut self, mut start: u16, data: &[Option<u16>]) {
414 let mem = &mut self.0;
415
416 // chunk_by was added in Rust 1.77
417 struct ChunkBy<'s, T, F>(&'s [T], F);
418 impl<'s, T, F: FnMut(&T, &T) -> bool> Iterator for ChunkBy<'s, T, F> {
419 type Item = &'s [T];
420
421 fn next(&mut self) -> Option<Self::Item> {
422 let (first, rest) = self.0.split_first()?;
423
424 // find the first element that doesn't match pred (+1 for the first el that was removed)
425 let pos = match rest.iter().position(|n| !(self.1)(first, n)) {
426 Some(i) => i + 1,
427 None => self.0.len(),
428 };
429
430 let (chunk, rest) = self.0.split_at(pos);
431
432 self.0 = rest;
433 Some(chunk)
434 }
435 }
436
437 // separate data into chunks of initialized/uninitialized
438 for chunk in ChunkBy(data, |a: &Option<_>, b: &Option<_>| a.is_some() == b.is_some()) {
439 let end = start.wrapping_add(chunk.len() as u16);
440
441 let si = usize::from(start);
442 let ei = usize::from(end);
443 let block_is_contiguous = start <= end;
444
445 if chunk[0].is_some() { // if chunk is init, copy the data over
446 let ch: Vec<_> = chunk.iter()
447 .map(|&opt| opt.unwrap())
448 .map(Word::new_init)
449 .collect();
450
451 if block_is_contiguous {
452 mem[si..ei].copy_from_slice(&ch);
453 } else {
454 let (left, right) = ch.split_at(start.wrapping_neg() as usize);
455 mem[si..].copy_from_slice(left);
456 mem[..ei].copy_from_slice(right)
457 }
458 } else { // if chunk is uninit, clear the initialization state
459 if block_is_contiguous {
460 for word in &mut mem[si..ei] {
461 word.clear_init();
462 }
463 } else {
464 for word in &mut mem[si..] {
465 word.clear_init();
466 }
467 for word in &mut mem[..ei] {
468 word.clear_init();
469 }
470 }
471 }
472
473 start = end;
474 }
475 }
476
477 pub(super) fn as_slice_mut(&mut self) -> &mut [Word] {
478 &mut *self.0
479 }
480}
481impl std::ops::Index<u16> for MemArray {
482 type Output = Word;
483
484 fn index(&self, index: u16) -> &Self::Output {
485 &self.0[index as usize]
486 }
487}
488impl std::ops::IndexMut<u16> for MemArray {
489 fn index_mut(&mut self, index: u16) -> &mut Self::Output {
490 &mut self.0[index as usize]
491 }
492}
493
494/// The register file.
495///
496/// This struct can be indexed with a [`Reg`].
497///
498/// # Example
499///
500/// ```
501/// use lc3_ensemble::sim::mem::RegFile;
502/// use lc3_ensemble::ast::Reg::R0;
503///
504/// let mut reg = RegFile::new(&mut ()); // never should have to initialize a reg file
505/// reg[R0].set(11);
506/// assert_eq!(reg[R0].get(), 11);
507/// ```
508#[derive(Debug, Clone)]
509pub struct RegFile([Word; 8]);
510impl RegFile {
511 /// Creates a register file with uninitialized data.
512 pub fn new(filler: &mut impl WordFiller) -> Self {
513 Self(filler.generate_array())
514 }
515}
516impl std::ops::Index<Reg> for RegFile {
517 type Output = Word;
518
519 fn index(&self, index: Reg) -> &Self::Output {
520 &self.0[usize::from(index)]
521 }
522}
523impl std::ops::IndexMut<Reg> for RegFile {
524 fn index_mut(&mut self, index: Reg) -> &mut Self::Output {
525 &mut self.0[usize::from(index)]
526 }
527}