glulx_asm/
operands.rs

1// SPDX-License-Identifier: Apache-2.0 WITH LLVM-Exception
2// Copyright 2024 Daniel Fox Franke.
3
4//! Types related to instruction operands.
5
6use core::fmt::Display;
7
8use bytes::BufMut;
9
10use crate::{
11    cast::CastSign,
12    error::AssemblerError,
13    resolver::{ResolvedAddr, Resolver},
14    LabelRef,
15};
16
17/// An operand indicating where to get a value from.
18///
19/// For variants that accept a label+offset, it is unsupported to provide a
20/// label in RAM with an offset that points backward into ROM. This will return
21/// an overflow error when you try to assemble it.
22#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
23pub enum LoadOperand<L> {
24    /// Pop the value from the stack.
25    Pop,
26    /// Use the immediate value of the operand.
27    Imm(i32),
28    /// Load the value from the stack at the given offset from the frame
29    /// pointer.
30    FrameAddr(u32),
31    /// Use the address corresponding to the given label+offset and right-shift
32    /// as an immediate value.
33    ///
34    /// Generating an operand with a right-shift of 1 or 2 is useful with the
35    /// array load/store instructions, allowing an unaligned access to an
36    /// aligned array, as opposed to the usual pattern of an aligned access to
37    /// an unaligned array. The shift is computed *after* the offset, *i.e.*,
38    /// the offset is still given in bytes. Shifting a label by more than its
39    /// alignment will produce an error at assembly time.
40    ImmLabel(LabelRef<L>, u8),
41    /// Load the value from the address at the given label+offset.
42    DerefLabel(LabelRef<L>),
43    /// Compute an offset in order for a branch instruction to jump to the given
44    /// label.
45    ///
46    /// When this label is resolved, the computed offset will be relative to the
47    /// end of the *operand*. Jumps are computed relative to the end of the
48    /// *instruction*. Fortunately, these are one-in-the-same, because every
49    /// operand that Glulx interprets as an offset is the last operand of the
50    /// instruction in which it occurs. The assembler won't stop you from using
51    /// this variant in other locations. If you do, you'll get a nonsensical
52    /// result, but you were already doing something nonsensical so GIGO.
53    Branch(L),
54}
55
56/// An operand indicating where to put a value.
57#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
58pub enum StoreOperand<L> {
59    /// Push the value to the stack.
60    Push,
61    /// Discard the value.
62    Discard,
63    /// Store the value to the stack address at the given offset from the frame
64    /// pointer.
65    FrameAddr(u32),
66    /// Store the value to the address given by the label+offset.
67    DerefLabel(LabelRef<L>),
68}
69
70/// An encoded operand ready to be serialized.
71#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
72pub(crate) enum RawOperand {
73    /// 0: Load zero, or discard store
74    Null,
75    /// 1: Constant, -80 to 7F
76    Imm8(i8),
77    /// 2: Constant, -8000 to 7FFFF
78    Imm16(i16),
79    /// 3: Constant, any value
80    Imm32(i32),
81    /// 5: Contents of address 00 to FF
82    Addr8(u8),
83    /// 6: Contents of address 0000 to FFFF
84    Addr16(u16),
85    /// 7: Contents of any address
86    Addr32(u32),
87    /// 8: Value pushed/popped off stack
88    Stack,
89    /// 9: Call frame local at address 00 to FF
90    Frame8(u8),
91    /// A: Call frame local at address 0000 to FFFF
92    Frame16(u16),
93    /// B: Call frame local at any address
94    Frame32(u32),
95    /// D: Contents of RAM address 00 to FF
96    Ram8(u8),
97    /// E: Contents of RAM address 0000 to FFFF
98    Ram16(u16),
99    /// F: Contents of RAM, any address
100    Ram32(u32),
101}
102
103impl<L> LoadOperand<L> {
104    /// Applies the given mapping function to the label (if any) within the operand.
105    pub fn map<F, M>(self, mut f: F) -> LoadOperand<M>
106    where
107        F: FnMut(L) -> M,
108    {
109        match self {
110            LoadOperand::Pop => LoadOperand::Pop,
111            LoadOperand::Imm(x) => LoadOperand::Imm(x),
112            LoadOperand::FrameAddr(p) => LoadOperand::FrameAddr(p),
113            LoadOperand::ImmLabel(l, shift) => LoadOperand::ImmLabel(l.map(f), shift),
114            LoadOperand::DerefLabel(l) => LoadOperand::DerefLabel(l.map(f)),
115            LoadOperand::Branch(l) => LoadOperand::Branch(f(l)),
116        }
117    }
118}
119
120impl<L> LoadOperand<L>
121where
122    L: Clone,
123{
124    /// Resolve labels in the operand, provided that the operand occurs at the
125    /// given position and RAM begins at the given address.
126    pub(crate) fn resolve<R>(
127        &self,
128        position: u32,
129        resolver: &R,
130    ) -> Result<RawOperand, AssemblerError<L>>
131    where
132        R: Resolver<Label = L>,
133    {
134        Ok(match self {
135            LoadOperand::Pop => RawOperand::Stack,
136            LoadOperand::Imm(x) => {
137                if *x == 0 {
138                    RawOperand::Null
139                } else if let Ok(x) = i8::try_from(*x) {
140                    RawOperand::Imm8(x)
141                } else if let Ok(x) = i16::try_from(*x) {
142                    RawOperand::Imm16(x)
143                } else {
144                    RawOperand::Imm32(*x)
145                }
146            }
147            LoadOperand::FrameAddr(x) => {
148                if let Ok(x) = u8::try_from(*x) {
149                    RawOperand::Frame8(x)
150                } else if let Ok(x) = u16::try_from(*x) {
151                    RawOperand::Frame16(x)
152                } else {
153                    RawOperand::Frame32(*x)
154                }
155            }
156            LoadOperand::ImmLabel(l, shift) => {
157                let unshifted_addr = l.resolve_absolute(resolver)?;
158                if unshifted_addr.trailing_zeros() < (*shift).into() {
159                    return Err(AssemblerError::InsufficientAlignment {
160                        label: l.0.clone(),
161                        offset: l.1,
162                        shift: *shift,
163                    });
164                }
165
166                let addr = (unshifted_addr >> *shift).cast_sign();
167
168                if addr == 0 {
169                    RawOperand::Null
170                } else if let Ok(x) = i8::try_from(addr) {
171                    RawOperand::Imm8(x)
172                } else if let Ok(x) = i16::try_from(addr) {
173                    RawOperand::Imm16(x)
174                } else {
175                    RawOperand::Imm32(addr)
176                }
177            }
178            LoadOperand::DerefLabel(l) => match l.resolve(resolver)? {
179                ResolvedAddr::Rom(addr) => {
180                    if let Ok(x) = u8::try_from(addr) {
181                        RawOperand::Addr8(x)
182                    } else if let Ok(x) = u16::try_from(addr) {
183                        RawOperand::Addr16(x)
184                    } else {
185                        RawOperand::Addr32(addr)
186                    }
187                }
188                ResolvedAddr::Ram(ramaddr) => {
189                    // An offset in RAM which points backward into ROM is
190                    // intentionally unsupported here.
191                    if let Ok(x) = u8::try_from(ramaddr) {
192                        RawOperand::Ram8(x)
193                    } else if let Ok(x) = u16::try_from(ramaddr) {
194                        RawOperand::Ram16(x)
195                    } else {
196                        RawOperand::Ram32(ramaddr)
197                    }
198                }
199            },
200            LoadOperand::Branch(l) => {
201                let target = resolver.resolve_absolute(l)?;
202
203                // We have to be careful here not to shrink an operand in such a
204                // way as it has to grow again because shrinking it increased
205                // the offset. Also not to accidentally generate 0 or 1 as an
206                // offset, which are interpreted specially by Glulx. So, first
207                // pick an operand size starting with 1, compute the resulting
208                // offset based on that size, and see if it would fit and not be
209                // 0/1.  If not, move on to the next larger size.
210
211                let null_offset = (target.cast_sign())
212                    .wrapping_sub(position.cast_sign())
213                    .wrapping_add(2);
214
215                let i8_offset = null_offset.wrapping_sub(1);
216                if let Ok(x) = i8::try_from(i8_offset) {
217                    if x != 0 && x != 1 {
218                        return Ok(RawOperand::Imm8(x));
219                    }
220                }
221
222                let i16_offset = null_offset.wrapping_sub(2);
223                if let Ok(x) = i16::try_from(i16_offset) {
224                    if x != 0 && x != 1 {
225                        return Ok(RawOperand::Imm16(x));
226                    }
227                }
228
229                let i32_offset = null_offset.wrapping_sub(4);
230                // Shouldn't be possible with a 4-byte operand because we'd be
231                // jumping into the middle of the operand.
232                assert!(i32_offset != 0 && i32_offset != 1);
233                return Ok(RawOperand::Imm32(i32_offset));
234            }
235        })
236    }
237
238    /// Returns an upper bound on how long this operand can end up being,
239    /// regardless of where it's placed.
240    pub(crate) fn worst_len(&self) -> usize {
241        match self {
242            LoadOperand::Pop => 0,
243            LoadOperand::Imm(x) => {
244                if *x == 0 {
245                    0
246                } else if i8::try_from(*x).is_ok() {
247                    1
248                } else if i16::try_from(*x).is_ok() {
249                    2
250                } else {
251                    4
252                }
253            }
254            LoadOperand::FrameAddr(x) => {
255                if u8::try_from(*x).is_ok() {
256                    1
257                } else if u16::try_from(*x).is_ok() {
258                    2
259                } else {
260                    4
261                }
262            }
263            LoadOperand::ImmLabel(_, _) => 4,
264            LoadOperand::DerefLabel(_) => 4,
265            LoadOperand::Branch(_) => 4,
266        }
267    }
268}
269
270impl<L> Display for LoadOperand<L>
271where
272    L: Display,
273{
274    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
275        match self {
276            LoadOperand::Pop => f.write_str("pop")?,
277            LoadOperand::Imm(x) => write!(f, "{x:#x}")?,
278            LoadOperand::FrameAddr(a) => {
279                write!(f, "${}", a / 4)?;
280                if a % 4 != 0 {
281                    write!(f, ".{}", a % 4)?;
282                }
283            }
284            LoadOperand::ImmLabel(LabelRef(label, offset), shift) => {
285                write!(f, "({label}")?;
286                if *offset != 0 {
287                    write!(f, "{offset:+#x}")?;
288                }
289                if *shift != 0 {
290                    write!(f, ">>{shift}")?;
291                }
292                write!(f, ")")?;
293            }
294            LoadOperand::DerefLabel(LabelRef(label, offset)) => {
295                write!(f, "[{label}")?;
296                if *offset != 0 {
297                    write!(f, "{offset:+#x}")?;
298                }
299                write!(f, "]")?;
300            }
301            LoadOperand::Branch(label) => {
302                write!(f, "~({label})")?;
303            }
304        };
305        Ok(())
306    }
307}
308
309impl<L> StoreOperand<L> {
310    /// Applies the given mapping function to the label (if any) within the operand.
311    pub fn map<F, M>(self, f: F) -> StoreOperand<M>
312    where
313        F: FnMut(L) -> M,
314    {
315        match self {
316            StoreOperand::Push => StoreOperand::Push,
317            StoreOperand::Discard => StoreOperand::Discard,
318            StoreOperand::FrameAddr(x) => StoreOperand::FrameAddr(x),
319            StoreOperand::DerefLabel(l) => StoreOperand::DerefLabel(l.map(f)),
320        }
321    }
322}
323
324impl<L> StoreOperand<L>
325where
326    L: Clone,
327{
328    /// Resolve labels in the operand, provided that the operand occurs at the
329    /// given position and RAM begins at the given address. These arguments are
330    /// in fact ignored, but we need this type signature to be the same as the
331    /// one for [`LoadOperand::resolve`] in order for our macros to work.
332    pub(crate) fn resolve<R>(
333        &self,
334        _position: u32,
335        resolver: &R,
336    ) -> Result<RawOperand, AssemblerError<L>>
337    where
338        R: Resolver<Label = L>,
339    {
340        Ok(match self {
341            StoreOperand::Push => RawOperand::Stack,
342            StoreOperand::Discard => RawOperand::Null,
343            StoreOperand::FrameAddr(x) => {
344                if let Ok(x) = u8::try_from(*x) {
345                    RawOperand::Frame8(x)
346                } else if let Ok(x) = u16::try_from(*x) {
347                    RawOperand::Frame16(x)
348                } else {
349                    RawOperand::Frame32(*x)
350                }
351            }
352            StoreOperand::DerefLabel(l) => match l.resolve(resolver)? {
353                ResolvedAddr::Rom(addr) => {
354                    if let Ok(x) = u8::try_from(addr) {
355                        RawOperand::Addr8(x)
356                    } else if let Ok(x) = u16::try_from(addr) {
357                        RawOperand::Addr16(x)
358                    } else {
359                        RawOperand::Addr32(addr)
360                    }
361                }
362                ResolvedAddr::Ram(addr) => {
363                    if let Ok(x) = u8::try_from(addr) {
364                        RawOperand::Ram8(x)
365                    } else if let Ok(x) = u16::try_from(addr) {
366                        RawOperand::Ram16(x)
367                    } else {
368                        RawOperand::Ram32(addr)
369                    }
370                }
371            },
372        })
373    }
374
375    /// Returns an upper bound on how long this operand can end up being,
376    /// regardless of where it's placed.
377    pub(crate) fn worst_len(&self) -> usize {
378        match self {
379            StoreOperand::Push => 0,
380            StoreOperand::Discard => 0,
381            StoreOperand::FrameAddr(x) => {
382                if u8::try_from(*x).is_ok() {
383                    1
384                } else if u16::try_from(*x).is_ok() {
385                    2
386                } else {
387                    4
388                }
389            }
390            StoreOperand::DerefLabel(_) => 4,
391        }
392    }
393}
394
395impl<L> Display for StoreOperand<L>
396where
397    L: Display,
398{
399    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
400        match self {
401            StoreOperand::Push => f.write_str("push")?,
402            StoreOperand::Discard => f.write_str("discard")?,
403            StoreOperand::FrameAddr(a) => {
404                write!(f, "${}", a / 4)?;
405                if a % 4 != 0 {
406                    write!(f, ".{}", a % 4)?;
407                }
408            }
409            StoreOperand::DerefLabel(LabelRef(label, offset)) => {
410                write!(f, "[{label}")?;
411                if *offset != 0 {
412                    write!(f, "{offset:+#x}")?;
413                }
414                write!(f, "]")?;
415            }
416        }
417        Ok(())
418    }
419}
420
421impl RawOperand {
422    /// Returns the encoded length of the operand.
423    pub(crate) fn len(&self) -> usize {
424        match self {
425            RawOperand::Null => 0,
426            RawOperand::Imm8(_) => 1,
427            RawOperand::Imm16(_) => 2,
428            RawOperand::Imm32(_) => 4,
429            RawOperand::Addr8(_) => 1,
430            RawOperand::Addr16(_) => 2,
431            RawOperand::Addr32(_) => 4,
432            RawOperand::Stack => 0,
433            RawOperand::Frame8(_) => 1,
434            RawOperand::Frame16(_) => 2,
435            RawOperand::Frame32(_) => 4,
436            RawOperand::Ram8(_) => 1,
437            RawOperand::Ram16(_) => 2,
438            RawOperand::Ram32(_) => 4,
439        }
440    }
441
442    /// Returns the addressing-mode nibble.
443    pub(crate) fn mode(&self) -> u8 {
444        match self {
445            RawOperand::Null => 0,
446            RawOperand::Imm8(_) => 1,
447            RawOperand::Imm16(_) => 2,
448            RawOperand::Imm32(_) => 3,
449            RawOperand::Addr8(_) => 5,
450            RawOperand::Addr16(_) => 6,
451            RawOperand::Addr32(_) => 7,
452            RawOperand::Stack => 8,
453            RawOperand::Frame8(_) => 9,
454            RawOperand::Frame16(_) => 0xa,
455            RawOperand::Frame32(_) => 0xb,
456            RawOperand::Ram8(_) => 0xd,
457            RawOperand::Ram16(_) => 0xe,
458            RawOperand::Ram32(_) => 0xf,
459        }
460    }
461
462    /// Serializes the operand.
463    pub(crate) fn serialize<B: BufMut>(&self, mut buf: B) {
464        match self {
465            RawOperand::Null => {}
466            RawOperand::Imm8(x) => buf.put_i8(*x),
467            RawOperand::Imm16(x) => buf.put_i16(*x),
468            RawOperand::Imm32(x) => buf.put_i32(*x),
469            RawOperand::Addr8(x) => buf.put_u8(*x),
470            RawOperand::Addr16(x) => buf.put_u16(*x),
471            RawOperand::Addr32(x) => buf.put_u32(*x),
472            RawOperand::Stack => {}
473            RawOperand::Frame8(x) => buf.put_u8(*x),
474            RawOperand::Frame16(x) => buf.put_u16(*x),
475            RawOperand::Frame32(x) => buf.put_u32(*x),
476            RawOperand::Ram8(x) => buf.put_u8(*x),
477            RawOperand::Ram16(x) => buf.put_u16(*x),
478            RawOperand::Ram32(x) => buf.put_u32(*x),
479        }
480    }
481}
482
483/// Creates an immediate operand out of the given `f32`.
484#[inline]
485pub fn f32_to_imm<L>(x: f32) -> LoadOperand<L> {
486    LoadOperand::Imm(x.to_bits().cast_sign())
487}
488
489/// Creates a pair of immediate operands out of the given `f64`, returned as (hi,lo).
490#[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
491#[inline]
492pub fn f64_to_imm<L>(x: f64) -> (LoadOperand<L>, LoadOperand<L>) {
493    let n = x.to_bits();
494    let high = (n >> 32) as u32;
495    let low = n as u32;
496    (
497        LoadOperand::Imm(high.cast_sign()),
498        LoadOperand::Imm(low.cast_sign()),
499    )
500}