glulx_asm/
assemble.rs

1// SPDX-License-Identifier: Apache-2.0 WITH LLVM-Exception
2// Copyright 2024 Daniel Fox Franke.
3
4//! Main assembler implementation.
5
6use alloc::borrow::{Borrow, Cow};
7use bytes::{Buf, BufMut, Bytes, BytesMut};
8use core::{fmt::Display, hash::Hash};
9
10#[cfg(not(feature = "std"))]
11use hashbrown::HashMap;
12#[cfg(feature = "std")]
13use std::collections::HashMap;
14
15use crate::{
16    cast::{checked_next_multiple_of, Overflow},
17    error::AssemblerError,
18    items::{Item, LabelRef, ZeroItem},
19    resolver::{ResolvedAddr, Resolver},
20};
21
22/// Length of the story file header.
23const HEADER_LENGTH: u32 = 0x24;
24/// Magic number identifying a Glulx story file.
25const MAGIC_NUMBER: u32 = 0x476C756C;
26/// The Glulx version we're implementing (3.1.3).
27const GLULX_VERSION: u32 = 0x00030103;
28
29/// Resolve labels from a hash table lookup.
30struct HashResolver<'a, L> {
31    hashmap: &'a HashMap<L, u32>,
32    ramstart: u32,
33}
34
35impl<L> Resolver for HashResolver<'_, L>
36where
37    L: Clone + Hash + Eq,
38{
39    type Label = L;
40
41    fn resolve(&self, label: &Self::Label) -> Result<ResolvedAddr, AssemblerError<Self::Label>> {
42        let addr = self.resolve_absolute(label)?;
43        if addr < self.ramstart {
44            Ok(ResolvedAddr::Rom(addr))
45        } else {
46            Ok(ResolvedAddr::Ram(addr - self.ramstart))
47        }
48    }
49
50    fn resolve_absolute(&self, label: &Self::Label) -> Result<u32, AssemblerError<Self::Label>> {
51        Ok(*self
52            .hashmap
53            .get(label)
54            .ok_or_else(|| AssemblerError::UndefinedLabel(label.clone()))?)
55    }
56}
57
58/// Collection of all inputs needed to assemble a story file.
59#[derive(Debug, Clone)]
60pub struct Assembly<'a, L>
61where
62    L: Clone,
63{
64    /// List of items which should appear in the ROM section.
65    pub rom_items: Cow<'a, [Item<L>]>,
66    /// List of items which should appear in the RAM section.
67    pub ram_items: Cow<'a, [Item<L>]>,
68    /// List of items which should have space in the RAM section and be initialized to zero.
69    pub zero_items: Cow<'a, [ZeroItem<L>]>,
70    /// How much space to allocate for the stack.
71    pub stack_size: u32,
72    /// Reference to the function to be called at the start of execution.
73    pub start_func: LabelRef<L>,
74    /// Reference to the initial decoding table.
75    pub decoding_table: Option<LabelRef<L>>,
76}
77
78impl<L> Assembly<'_, L>
79where
80    L: Clone + Eq + Hash,
81{
82    /// Applies the given mapping function to all labels within the assembly.
83    pub fn map<F, M>(self, mut f: F) -> Assembly<'static, M>
84    where
85        F: FnMut(L) -> M,
86        M: Clone,
87    {
88        let rom_items = self
89            .rom_items
90            .iter()
91            .cloned()
92            .map(|item| item.map(&mut f))
93            .collect();
94
95        let ram_items = self
96            .ram_items
97            .iter()
98            .cloned()
99            .map(|item| item.map(&mut f))
100            .collect();
101
102        let zero_items = self
103            .zero_items
104            .iter()
105            .cloned()
106            .map(|item| item.map(&mut f))
107            .collect();
108
109        let stack_size = self.stack_size;
110
111        let start_func = self.start_func.map(&mut f);
112        let decoding_table = self.decoding_table.map(|r| r.map(&mut f));
113
114        Assembly {
115            rom_items,
116            ram_items,
117            zero_items,
118            stack_size,
119            start_func,
120            decoding_table,
121        }
122    }
123
124    /// Assembles a Glulx binary, ready to be written out as a `.ulx` file.
125    pub fn assemble(&self) -> Result<BytesMut, AssemblerError<L>> {
126        assemble(
127            self.rom_items.borrow(),
128            self.ram_items.borrow(),
129            self.zero_items.borrow(),
130            self.stack_size,
131            &self.start_func,
132            &self.decoding_table,
133        )
134    }
135
136    /// Converts all internal [`Cow`] fields to owned.
137    pub fn to_owning(&self) -> Assembly<'static, L> {
138        Assembly {
139            rom_items: Cow::Owned(self.rom_items.clone().into_owned()),
140            ram_items: Cow::Owned(self.ram_items.clone().into_owned()),
141            zero_items: Cow::Owned(self.zero_items.clone().into_owned()),
142            stack_size: self.stack_size,
143            start_func: self.start_func.clone(),
144            decoding_table: self.decoding_table.clone(),
145        }
146    }
147}
148
149impl<L> Display for Assembly<'_, L>
150where
151    L: Display + Clone,
152{
153    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
154        writeln!(f, ".stack_size {}", self.stack_size)?;
155        write!(f, ".start_func ({}", self.start_func.0)?;
156        if self.start_func.1 != 0 {
157            write!(f, "{:+#x}", self.start_func.1)?;
158        }
159        writeln!(f, ")")?;
160        if let Some(decoding_table) = &self.decoding_table {
161            write!(f, ".initial_decoding_table ({}", decoding_table.0)?;
162            if decoding_table.1 != 0 {
163                write!(f, "{:+#x}", decoding_table.1)?;
164            }
165            writeln!(f, ")")?;
166        }
167        for item in self.rom_items.iter() {
168            writeln!(f, "{item}")?;
169        }
170        writeln!(f, ".ram_items")?;
171        for item in self.ram_items.iter() {
172            writeln!(f, "{item}")?;
173        }
174        writeln!(f, ".zero_items")?;
175        for item in self.zero_items.iter() {
176            writeln!(f, "{item}")?;
177        }
178        Ok(())
179    }
180}
181
182/// Top-level function of our main assembler algorithm.
183///
184/// The hard part of this is dealing with variable-length operands, and
185/// especially dealing with the PC-relative offset operands used by branch
186/// instructions. The approach is basically:
187///
188/// 1. Start by computing label positions based on the worst case in which every
189///    label-based operand requires full-width encoding.
190///
191/// 2. Compute encoding lengths based on everything being at the positions we
192///    computed in step 1, and then compute new positions based on those
193///    lengths.
194///
195/// 3. Repeat step 2 based on the results from the previous iteration, and keep
196///    iterating until we get to a fixed point.
197///
198///    Operands should only ever shrink, lengths are natural numbers, and the
199///    natural numbers are well-ordered, so the Tarski fixed-point theorem
200///    should guarantee termination. Making sure of "operands should only ever
201///    shrink" is a little tricky, because we have to be careful that, as a
202///    result of shrinking a branch-offset operand, it gets further away from
203///    the thing it's branching to and so it needs to grow again to encode the
204///    larger offset. But that trickiness is handled in
205///    [`LoadOperand::resolve`], not in this function.
206///
207/// 4. Finally, serialize the output, checking assertions along the way to make
208///    sure the lengths we got are the ones we planned to get.
209fn assemble<L>(
210    rom_items: &[Item<L>],
211    ram_items: &[Item<L>],
212    zero_items: &[ZeroItem<L>],
213    stack_size: u32,
214    start_func: &LabelRef<L>,
215    decoding_table: &Option<LabelRef<L>>,
216) -> Result<BytesMut, AssemblerError<L>>
217where
218    L: Clone + Eq + Hash,
219{
220    let mut labeled: HashMap<L, u32> = HashMap::new();
221
222    let mut position = HEADER_LENGTH;
223
224    // Step 1: initialize positions
225    initialize_positions(rom_items, &mut labeled, &mut position)?;
226    position = checked_next_multiple_of(position, 256)?;
227    let mut ramstart = position;
228    initialize_positions(ram_items, &mut labeled, &mut position)?;
229    position = checked_next_multiple_of(position, 256)?;
230    initialize_zero_positions(zero_items, &mut labeled, &mut position)?;
231
232    // Step 2/3: update positions until we reach a fixed point.
233    loop {
234        position = HEADER_LENGTH;
235
236        let rom_improved = update_positions(rom_items, &mut labeled, &mut position, ramstart)?;
237        position = checked_next_multiple_of(position, 256)?;
238        ramstart = position;
239        let ram_improved = update_positions(ram_items, &mut labeled, &mut position, ramstart)?;
240        position = checked_next_multiple_of(position, 256)?;
241        let zero_improved = update_zero_positions(zero_items, &mut labeled, &mut position)?;
242
243        if !rom_improved && !ram_improved && !zero_improved {
244            break;
245        }
246    }
247
248    // Step 4: serialize output.
249    let mut body = BytesMut::new();
250    serialize_items(rom_items, &labeled, ramstart, &mut body)?;
251    assert_eq!(
252        ramstart
253            .checked_sub(HEADER_LENGTH)
254            .expect("ramstart should be >= HEADER_LENGTH"),
255        body.len().try_into().overflow()?
256    );
257    serialize_items(ram_items, &labeled, ramstart, &mut body)?;
258
259    let body = body.freeze();
260    let extstart = u32::try_from(body.len())
261        .overflow()?
262        .checked_add(HEADER_LENGTH)
263        .overflow()?;
264
265    let endmem = checked_next_multiple_of(verify_zero_items(zero_items, &labeled, extstart)?, 256)?;
266
267    let resolver = HashResolver {
268        hashmap: &labeled,
269        ramstart,
270    };
271
272    let resolved_decoding_table = if let Some(decoding_table) = decoding_table {
273        decoding_table.resolve_absolute(&resolver)?
274    } else {
275        0u32
276    };
277
278    let resolved_start_func: u32 = start_func.resolve_absolute(&resolver)?;
279
280    let sum = MAGIC_NUMBER
281        .wrapping_add(GLULX_VERSION)
282        .wrapping_add(ramstart)
283        .wrapping_add(extstart)
284        .wrapping_add(endmem)
285        .wrapping_add(stack_size)
286        .wrapping_add(resolved_start_func)
287        .wrapping_add(resolved_decoding_table)
288        .wrapping_add(checksum(body.clone()));
289
290    let mut output = BytesMut::with_capacity(
291        body.len()
292            .checked_add(
293                usize::try_from(HEADER_LENGTH).expect("u32 to usize conversion should succeed"),
294            )
295            .overflow()?,
296    );
297
298    output.put_u32(MAGIC_NUMBER);
299    output.put_u32(GLULX_VERSION);
300    output.put_u32(ramstart);
301    output.put_u32(extstart);
302    output.put_u32(endmem);
303    output.put_u32(stack_size);
304    output.put_u32(resolved_start_func);
305    output.put_u32(resolved_decoding_table);
306    output.put_u32(sum);
307    output.put(body);
308
309    Ok(output)
310}
311
312/// Initializes item positions for the first step of assembly.
313fn initialize_positions<L>(
314    items: &[Item<L>],
315    labeled: &mut HashMap<L, u32>,
316    position: &mut u32,
317) -> Result<(), AssemblerError<L>>
318where
319    L: Clone + Hash + Eq,
320{
321    for item in items {
322        let worst_len: u32 = item.worst_len().try_into().overflow()?;
323        let end_position = position.checked_add(worst_len).overflow()?;
324        if let Item::Label(label) = item {
325            if labeled.insert(label.clone(), *position).is_some() {
326                return Err(AssemblerError::DuplicateLabel(label.clone()));
327            }
328        }
329
330        *position = checked_next_multiple_of(end_position, item.align())?;
331    }
332
333    Ok(())
334}
335
336/// Initializes zero-item positions for the first step of assembly.
337fn initialize_zero_positions<L>(
338    items: &[ZeroItem<L>],
339    labeled: &mut HashMap<L, u32>,
340    position: &mut u32,
341) -> Result<(), AssemblerError<L>>
342where
343    L: Clone + Hash + Eq,
344{
345    for item in items {
346        let end_position = position.checked_add(item.len()).overflow()?;
347        if let ZeroItem::Label(label) = item {
348            if labeled.insert(label.clone(), *position).is_some() {
349                return Err(AssemblerError::DuplicateLabel(label.clone()));
350            };
351        }
352
353        *position = checked_next_multiple_of(end_position, item.align())?;
354    }
355
356    Ok(())
357}
358
359/// Called from each iterative step to update item positions.
360fn update_positions<L>(
361    items: &[Item<L>],
362    labeled: &mut HashMap<L, u32>,
363    position: &mut u32,
364    ramstart: u32,
365) -> Result<bool, AssemblerError<L>>
366where
367    L: Clone + Hash + Eq,
368{
369    let mut improvement_found = false;
370    for item in items {
371        let resolver = HashResolver {
372            hashmap: labeled,
373            ramstart,
374        };
375
376        let resolved_len = item.resolved_len(*position, &resolver)?;
377        let end_position = position
378            .checked_add(u32::try_from(resolved_len).overflow()?)
379            .overflow()?;
380
381        if let Item::Label(label) = item {
382            let old_position = *labeled
383                .get(label)
384                .expect("previously-inserted label should still be in the HashMap");
385
386            if *position != old_position {
387                improvement_found = true;
388                labeled.insert(label.clone(), *position);
389            }
390        }
391
392        *position = checked_next_multiple_of(end_position, item.align())?;
393    }
394    Ok(improvement_found)
395}
396
397/// Called from each iterative step to update zero-item positions.
398fn update_zero_positions<L>(
399    items: &[ZeroItem<L>],
400    labeled: &mut HashMap<L, u32>,
401    position: &mut u32,
402) -> Result<bool, AssemblerError<L>>
403where
404    L: Clone + Hash + Eq,
405{
406    let mut improvement_found = false;
407
408    for item in items {
409        let end_position = position.checked_add(item.len()).overflow()?;
410
411        if let ZeroItem::Label(label) = item {
412            let old_position = *labeled
413                .get(label)
414                .expect("previously-inserted label should still be in the HashMap");
415
416            if *position != old_position {
417                improvement_found = true;
418                labeled.insert(label.clone(), *position);
419            }
420        }
421
422        *position = checked_next_multiple_of(end_position, item.align())?;
423    }
424
425    Ok(improvement_found)
426}
427
428/// Serializes items after all final label positions have been computed.
429fn serialize_items<L>(
430    items: &[Item<L>],
431    labeled: &HashMap<L, u32>,
432    ramstart: u32,
433    buf: &mut BytesMut,
434) -> Result<(), AssemblerError<L>>
435where
436    L: Clone + Eq + Hash,
437{
438    for item in items {
439        let position = u32::try_from(buf.len())
440            .overflow()?
441            .checked_add(HEADER_LENGTH)
442            .overflow()?;
443
444        if let Item::Label(label) = item {
445            let expected_position = *labeled
446                .get(label)
447                .ok_or_else(|| AssemblerError::UndefinedLabel(label.clone()))?;
448            assert_eq!(
449                expected_position, position,
450                "label position should match previous calculation"
451            );
452        }
453
454        let resolver = HashResolver {
455            hashmap: labeled,
456            ramstart,
457        };
458
459        item.serialize(position, &resolver, &mut *buf)?;
460    }
461
462    let position = u32::try_from(buf.len())
463        .overflow()?
464        .checked_add(HEADER_LENGTH)
465        .overflow()?;
466
467    let page_offset = position % 256;
468    let padding = usize::try_from(if page_offset == 0 {
469        0
470    } else {
471        256 - page_offset
472    })
473    .expect("u32 to usize conversion should succeed");
474    buf.put_bytes(0, padding);
475
476    Ok(())
477}
478
479/// Checks assertions to ensure that all zero-items were placed as intended.
480fn verify_zero_items<L>(
481    items: &[ZeroItem<L>],
482    labeled: &HashMap<L, u32>,
483    extstart: u32,
484) -> Result<u32, AssemblerError<L>>
485where
486    L: Clone + Eq + Hash,
487{
488    let mut position = extstart;
489
490    for item in items {
491        if let ZeroItem::Label(label) = item {
492            let expected_position = *labeled
493                .get(label)
494                .ok_or_else(|| AssemblerError::UndefinedLabel(label.clone()))?;
495            assert_eq!(
496                position, expected_position,
497                "label position should match previous calculation"
498            );
499        }
500
501        position = position.checked_add(item.len()).overflow()?;
502        position = checked_next_multiple_of(position, item.align())?;
503    }
504
505    Ok(position)
506}
507
508/// Header checksum calculation.
509fn checksum(mut bytes: Bytes) -> u32 {
510    let mut sum: u32 = 0;
511    while bytes.has_remaining() {
512        sum = sum.wrapping_add(bytes.get_u32());
513    }
514    sum
515}