• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

adierking / unplug / 25977721661

17 May 2026 01:12AM UTC coverage: 76.939%. First build
25977721661

push

github

adierking
asm: Automatically rewrite `lib` commands in globals

This fixes a problem Kobazco was running into where globals scripts that used
the `lib` command could never be loaded again.

29 of 53 new or added lines in 2 files covered. (54.72%)

19057 of 24769 relevant lines covered (76.94%)

1066209.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.89
/unplug-asm/src/program.rs
1
use crate::ast::IntValue;
2
use crate::label::{LabelId, LabelMap};
3
use crate::opcodes::{AsmMsgOp, DirOp, NamedOpcode};
4
use crate::span::{Span, Spanned};
5
use crate::{Error, Result};
6
use bitflags::bitflags;
7
use num_traits::NumCast;
8
use private::Sealed;
9
use smallvec::SmallVec;
10
use std::cmp::Ordering;
11
use std::collections::HashMap;
12
use std::fmt::{self, Display, Formatter};
13
use std::ops::{Deref, DerefMut};
14
use unplug::common::VecText;
15
use unplug::event::opcodes::{Atom, CmdOp, ExprOp, Opcode};
16
use unplug::event::BlockId;
17
use unplug::stage::Event;
18

19
mod private {
20
    pub trait Sealed {}
21
}
22

23
/// Wrapper which associates a value with a `Span` and implements `Spanned` for it.
24
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
25
pub struct Located<T> {
26
    value: T,
27
    span: Span,
28
}
29

30
impl<T> Located<T> {
31
    /// Creates a new located value with an empty span.
32
    pub fn new(value: T) -> Self {
4,161,990✔
33
        Self { value, span: Span::EMPTY }
4,161,990✔
34
    }
4,161,990✔
35

36
    /// Creates a new located value with the given span.
37
    pub fn with_span(value: T, span: Span) -> Self {
4,980,486✔
38
        Self { value, span }
4,980,486✔
39
    }
4,980,486✔
40

41
    /// Maps the inner value to a new one without changing the span.
42
    pub fn map<U, F>(self, f: F) -> Located<U>
1,397,937✔
43
    where
1,397,937✔
44
        F: FnOnce(T) -> U,
1,397,937✔
45
    {
46
        Located::with_span(f(self.value), self.span)
1,397,937✔
47
    }
1,397,937✔
48

49
    /// Returns the inner value.
50
    pub fn into_inner(self) -> T {
668,130✔
51
        self.value
668,130✔
52
    }
668,130✔
53
}
54

55
impl<T> Deref for Located<T> {
56
    type Target = T;
57
    fn deref(&self) -> &Self::Target {
18,312,642✔
58
        &self.value
18,312,642✔
59
    }
18,312,642✔
60
}
61

62
impl<T> DerefMut for Located<T> {
63
    fn deref_mut(&mut self) -> &mut Self::Target {
819,690✔
64
        &mut self.value
819,690✔
65
    }
819,690✔
66
}
67

68
impl<T> Spanned for Located<T> {
69
    fn span(&self) -> Span {
1,551,498✔
70
        self.span
1,551,498✔
71
    }
1,551,498✔
72
}
73

74
impl<T> From<T> for Located<T> {
75
    fn from(value: T) -> Self {
3,441,126✔
76
        Self::new(value)
3,441,126✔
77
    }
3,441,126✔
78
}
79

80
impl<T: Display> Display for Located<T> {
NEW
81
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
×
NEW
82
        self.value.fmt(f)
×
NEW
83
    }
×
84
}
85

86
/// Data which can be operated on.
87
#[derive(Debug, Clone)]
88
pub enum Operand {
89
    /// An 8-bit signed integer.
90
    I8(i8),
91
    /// An 8-bit unsigned integer.
92
    U8(u8),
93
    /// A 16-bit signed integer.
94
    I16(i16),
95
    /// A 16-bit unsigned integer.
96
    U16(u16),
97
    /// A 32-bit signed integer.
98
    I32(i32),
99
    /// A 32-bit unsigned integer.
100
    U32(u32),
101
    /// A printable text string.
102
    Text(VecText),
103
    /// A label reference.
104
    Label(LabelId),
105
    /// A label reference indicating it is an "else" condition.
106
    ElseLabel(LabelId),
107
    /// A raw file offset reference.
108
    Offset(u32),
109
    /// An atom expression.
110
    Atom(Atom),
111
    /// An expression.
112
    Expr(Box<Operation<ExprOp>>),
113
    /// A message command.
114
    MsgCommand(Box<Operation<AsmMsgOp>>),
115
    /// An intermediate error result used for partial compilation.
116
    Error,
117
}
118

119
impl Operand {
120
    /// Casts the operand to an integer type. Returns the integer on success, and returns an
121
    /// appropriate error on failure.
122
    pub fn cast<T: CastOperand>(&self) -> Result<T> {
861,654✔
123
        let result = match *self {
861,654✔
124
            Operand::I8(x) => T::from(x).ok_or_else(|| IntValue::I8(x.into())),
186✔
125
            Operand::U8(x) => T::from(x).ok_or_else(|| IntValue::U8(x.into())),
86,289✔
126
            Operand::I16(x) => T::from(x).ok_or_else(|| IntValue::I16(x.into())),
242,583✔
127
            Operand::U16(x) => T::from(x).ok_or_else(|| IntValue::U16(x.into())),
80,880✔
128
            Operand::I32(x) => T::from(x).ok_or(IntValue::I32(x)),
411,390✔
129
            Operand::U32(x) => T::from(x).ok_or(IntValue::U32(x)),
40,326✔
130
            _ => return Err(Error::ExpectedInteger),
×
131
        };
132
        result.map_err(T::error)
861,654✔
133
    }
861,654✔
134

135
    /// If the operand is a `Label`, returns the label ID, otherwise returns an appropriate error.
136
    pub fn label(&self) -> Result<LabelId> {
×
137
        match *self {
×
138
            Operand::Label(id) => Ok(id),
×
139
            Operand::ElseLabel(_) => Err(Error::UnexpectedElseLabel),
×
140
            _ => Err(Error::ExpectedLabel),
×
141
        }
142
    }
×
143
}
144

145
// `From` implementations for `Operand`
146
macro_rules! impl_operand_from {
147
    ($type:ty, $variant:ident) => {
148
        impl From<$type> for Operand {
149
            fn from(x: $type) -> Self {
19,530✔
150
                Self::$variant(x.into())
19,530✔
151
            }
19,530✔
152
        }
153
    };
154
}
155
impl_operand_from!(i8, I8);
156
impl_operand_from!(u8, U8);
157
impl_operand_from!(i16, I16);
158
impl_operand_from!(u16, U16);
159
impl_operand_from!(i32, I32);
160
impl_operand_from!(u32, U32);
161
impl_operand_from!(VecText, Text);
162
impl_operand_from!(Atom, Atom);
163
impl_operand_from!(Operation<ExprOp>, Expr);
164
impl_operand_from!(Operation<AsmMsgOp>, MsgCommand);
165

166
/// Trait for a primitive type which an operand can be cast to.
167
pub trait CastOperand: NumCast + Sealed {
168
    /// The number of bits in the integer.
169
    const BITS: usize;
170

171
    /// Maps `int` to an error corresponding to this type.
172
    fn error(int: IntValue) -> Error;
173
}
174

175
// `CastOperand` implementations
176
macro_rules! impl_cast_operand {
177
    ($type:ty, $err:path) => {
178
        impl Sealed for $type {}
179
        impl CastOperand for $type {
180
            const BITS: usize = std::mem::size_of::<$type>() * 8;
181
            fn error(int: IntValue) -> Error {
×
182
                $err(int)
×
183
            }
×
184
        }
185
    };
186
}
187
impl_cast_operand!(i8, Error::CannotConvertToI8);
188
impl_cast_operand!(u8, Error::CannotConvertToU8);
189
impl_cast_operand!(i16, Error::CannotConvertToI16);
190
impl_cast_operand!(u16, Error::CannotConvertToU16);
191
impl_cast_operand!(i32, Error::CannotConvertToI32);
192
impl_cast_operand!(u32, Error::CannotConvertToU32);
193

194
/// Operand type hints.
195
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
196
pub enum OperandType {
197
    /// The operand may be any type.
198
    Unknown,
199
    /// The operand must be a byte.
200
    Byte,
201
    /// The operand must be a word.
202
    Word,
203
    /// The operand must be a dword.
204
    Dword,
205
    /// The operand must be a message command.
206
    Message,
207
}
208

209
impl Display for OperandType {
210
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
×
211
        f.write_str(match *self {
×
212
            OperandType::Unknown => "unknown",
×
213
            OperandType::Byte => "byte",
×
214
            OperandType::Word => "word",
×
215
            OperandType::Dword => "dword",
×
216
            OperandType::Message => "message",
×
217
        })
218
    }
×
219
}
220

221
/// Trait for getting an operand type hint from an opcode.
222
pub trait TypeHint: Opcode {
223
    /// Returns the operand type hint corresponding to this opcode.
224
    fn type_hint(self) -> OperandType;
225
}
226

227
impl TypeHint for CmdOp {
228
    fn type_hint(self) -> OperandType {
452,385✔
229
        match self {
452,385✔
230
            CmdOp::Msg | CmdOp::Select => OperandType::Message,
21,462✔
231
            _ => OperandType::Unknown,
430,923✔
232
        }
233
    }
452,385✔
234
}
235

236
impl TypeHint for ExprOp {
237
    fn type_hint(self) -> OperandType {
165,336✔
238
        OperandType::Unknown
165,336✔
239
    }
165,336✔
240
}
241

242
impl TypeHint for DirOp {
243
    fn type_hint(self) -> OperandType {
8,451✔
244
        match self {
8,451✔
245
            DirOp::Byte => OperandType::Byte,
804✔
246
            DirOp::Word => OperandType::Word,
663✔
247
            DirOp::Dword => OperandType::Dword,
312✔
248
            _ => OperandType::Unknown,
6,672✔
249
        }
250
    }
8,451✔
251
}
252

253
impl TypeHint for AsmMsgOp {
254
    fn type_hint(self) -> OperandType {
79,782✔
255
        OperandType::Unknown
79,782✔
256
    }
79,782✔
257
}
258

259
/// An operation consisting of an opcode and zero or more operands.
260
#[derive(Debug, Clone)]
261
pub struct Operation<T: NamedOpcode> {
262
    pub opcode: Located<T>,
263
    pub operands: SmallVec<[Located<Operand>; 2]>,
264
}
265

266
impl<T: NamedOpcode> Operation<T> {
267
    /// Creates an empty operation with `opcode`.
268
    pub fn new(opcode: Located<T>) -> Self {
2,133,852✔
269
        Self { opcode, operands: SmallVec::new() }
2,133,852✔
270
    }
2,133,852✔
271

272
    /// Creates an empty operation with `opcode` and `operands`.
273
    pub fn with_operands(
688,314✔
274
        opcode: Located<T>,
688,314✔
275
        operands: impl IntoIterator<Item = Located<Operand>>,
688,314✔
276
    ) -> Self {
688,314✔
277
        Self { opcode, operands: operands.into_iter().collect() }
688,314✔
278
    }
688,314✔
279

280
    /// Replaces the operation's opcode without changing the span or operands.
281
    pub fn with_opcode<U: NamedOpcode>(self, opcode: U) -> Operation<U> {
×
282
        Operation { opcode: self.opcode.map(|_| opcode), operands: self.operands }
×
283
    }
×
284
}
285

286
impl<T: NamedOpcode> Spanned for Operation<T> {
287
    fn span(&self) -> Span {
6,672✔
288
        let operands = self.operands.iter().fold(Span::EMPTY, |s, o| s.join(o.span()));
12,873✔
289
        self.opcode.span().join(operands)
6,672✔
290
    }
6,672✔
291
}
292

293
/// Encapsulates possible code operation types.
294
pub enum CodeOperation {
295
    Command(Operation<CmdOp>),
296
    Expr(Operation<ExprOp>),
297
    MsgCommand(Operation<AsmMsgOp>),
298
}
299

300
impl CodeOperation {
301
    /// Appends `operand` onto the operation.
302
    pub fn push_operand(&mut self, operand: Located<Operand>) {
1,990,593✔
303
        match self {
1,990,593✔
304
            Self::Command(op) => op.operands.push(operand),
923,400✔
305
            Self::Expr(op) => op.operands.push(operand),
899,916✔
306
            Self::MsgCommand(op) => op.operands.push(operand),
167,277✔
307
        }
308
    }
1,990,593✔
309

310
    /// Consumes this wrapper and returns the inner command.
311
    /// ***Panics*** if the operation is not a command.
312
    pub fn into_command(self) -> Operation<CmdOp> {
452,385✔
313
        match self {
452,385✔
314
            Self::Command(op) => op,
452,385✔
315
            _ => panic!("expected a command operation"),
×
316
        }
317
    }
452,385✔
318

319
    /// Consumes this wrapper and returns the inner expression.
320
    /// ***Panics*** if the operation is not an expression.
321
    pub fn into_expr(self) -> Operation<ExprOp> {
845,283✔
322
        match self {
845,283✔
323
            Self::Expr(op) => op,
845,283✔
324
            _ => panic!("expected an expr operation"),
×
325
        }
326
    }
845,283✔
327

328
    /// Consumes this wrapper and returns the inner message command.
329
    /// ***Panics*** if the operation is not a message command.
330
    pub fn into_msg_command(self) -> Operation<AsmMsgOp> {
109,659✔
331
        match self {
109,659✔
332
            Self::MsgCommand(op) => op,
109,659✔
333
            _ => panic!("expected a message command"),
×
334
        }
335
    }
109,659✔
336
}
337

338
impl From<Operation<CmdOp>> for CodeOperation {
339
    fn from(op: Operation<CmdOp>) -> Self {
452,385✔
340
        Self::Command(op)
452,385✔
341
    }
452,385✔
342
}
343

344
impl From<Operation<ExprOp>> for CodeOperation {
345
    fn from(op: Operation<ExprOp>) -> Self {
845,283✔
346
        Self::Expr(op)
845,283✔
347
    }
845,283✔
348
}
349

350
impl From<Operation<AsmMsgOp>> for CodeOperation {
351
    fn from(op: Operation<AsmMsgOp>) -> Self {
109,659✔
352
        Self::MsgCommand(op)
109,659✔
353
    }
109,659✔
354
}
355

356
bitflags! {
357
    /// Flags used to mark blocks with hints for serialization/deserialization.
358
    #[derive(Default)]
359
    pub struct BlockFlags: u8 {
360
        /// The block is the beginning of a subroutine.
361
        const SUBROUTINE = 1 << 0;
362
        /// The block is associated with at least one entry point.
363
        const ENTRY_POINT = 1 << 1;
364
    }
365
}
366

367
/// Contents of a block.
368
#[derive(Debug, Clone)]
369
pub enum BlockContent {
370
    Code(Vec<Operation<CmdOp>>),
371
    Data(Vec<Located<Operand>>),
372
}
373

374
/// A block of instructions corresponding to a script block.
375
#[derive(Debug, Default, Clone)]
376
pub struct Block {
377
    /// The offset of the block in the original file (if known).
378
    pub offset: u32,
379
    /// Flags describing block properties.
380
    pub flags: BlockFlags,
381
    /// The block's content.
382
    pub content: Option<BlockContent>,
383
    /// The ID of the next block in program order, or `None` if this is the last block.
384
    pub next: Option<BlockId>,
385
}
386

387
impl Block {
388
    /// Creates an empty block.
389
    pub fn new() -> Self {
151,722✔
390
        Self::default()
151,722✔
391
    }
151,722✔
392

393
    /// Creates a code block containing `content`.
394
    pub fn with_content(content: BlockContent) -> Self {
35,208✔
395
        Self { content: Some(content), ..Default::default() }
35,208✔
396
    }
35,208✔
397

398
    /// Creates a code block populated from `commands`.
399
    pub fn with_code(commands: impl IntoIterator<Item = Operation<CmdOp>>) -> Self {
35,208✔
400
        Self::with_content(BlockContent::Code(commands.into_iter().collect()))
35,208✔
401
    }
35,208✔
402

403
    /// Creates a data block populated from `operands`.
404
    pub fn with_data(operands: impl IntoIterator<Item = Located<Operand>>) -> Self {
×
405
        Self::with_content(BlockContent::Data(operands.into_iter().collect()))
×
406
    }
×
407

408
    /// Returns true if there is nothing in the block.
409
    pub fn is_empty(&self) -> bool {
378,099✔
410
        match &self.content {
219,204✔
411
            None => true,
158,895✔
412
            Some(BlockContent::Code(c)) => c.is_empty(),
216,132✔
413
            Some(BlockContent::Data(d)) => d.is_empty(),
3,072✔
414
        }
415
    }
378,099✔
416

417
    /// Returns true if the block contains code.
418
    pub fn is_code(&self) -> bool {
1,779✔
419
        matches!(&self.content, Some(BlockContent::Code(_)))
1,779✔
420
    }
1,779✔
421

422
    /// Returns true if the block contains data.
423
    pub fn is_data(&self) -> bool {
529,818✔
424
        matches!(&self.content, Some(BlockContent::Data(_)))
528,858✔
425
    }
529,818✔
426

427
    /// Appends a command to the end of a code block. If the block is empty, it will become a code
428
    /// block. ***Panics*** if the block already contains data.
429
    pub fn push_command(&mut self, command: Operation<CmdOp>) {
452,385✔
430
        match self.content.get_or_insert(BlockContent::Code(vec![])) {
452,385✔
431
            BlockContent::Code(c) => c.push(command),
452,385✔
432
            BlockContent::Data(_) => panic!("cannot append a command to a data block"),
×
433
        }
434
    }
452,385✔
435

436
    /// Appends data to the end of a data block. If the block is empty, it will become a data block.
437
    /// ***Panics*** if the block already contains code.
438
    pub fn push_data(&mut self, data: impl IntoIterator<Item = Located<Operand>>) {
1,779✔
439
        match self.content.get_or_insert(BlockContent::Data(vec![])) {
1,779✔
440
            BlockContent::Code(_) => panic!("cannot append a command to a data block"),
×
441
            BlockContent::Data(d) => d.extend(data),
1,779✔
442
        }
443
    }
1,779✔
444
}
445

446
/// A kind of entry point into a program.
447
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
448
#[allow(variant_size_differences)]
449
pub enum EntryPoint {
450
    /// A global library function.
451
    Lib(i16),
452
    /// An event in a stage file.
453
    Event(Event),
454
}
455

456
impl EntryPoint {
457
    /// Returns the opcode of the directive which declares the entry point.
458
    pub fn directive(self) -> DirOp {
6,552✔
459
        match self {
5,424✔
460
            Self::Lib(_) => DirOp::Lib,
1,128✔
461
            Self::Event(Event::Prologue) => DirOp::Prologue,
42✔
462
            Self::Event(Event::Startup) => DirOp::Startup,
102✔
463
            Self::Event(Event::Dead) => DirOp::Dead,
66✔
464
            Self::Event(Event::Pose) => DirOp::Pose,
75✔
465
            Self::Event(Event::TimeCycle) => DirOp::TimeCycle,
60✔
466
            Self::Event(Event::TimeUp) => DirOp::TimeUp,
3✔
467
            Self::Event(Event::Interact(_)) => DirOp::Interact,
5,076✔
468
        }
469
    }
6,552✔
470
}
471

472
/// A target specifier indicating the purpose of a script.
473
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
474
pub enum Target {
475
    /// The script defines global library functions.
476
    Globals,
477
    /// The script is for the stage with the given name (no extension).
478
    Stage(String),
479
}
480

481
/// An assembly program.
482
#[derive(Default)]
483
pub struct Program {
484
    /// An optional target specifier.
485
    pub target: Option<Located<Target>>,
486
    /// The blocks making up the program. Each block's ID is its index in this list.
487
    pub blocks: Vec<Block>,
488
    /// The ID of the first block in the program, or `None` if the program is empty.
489
    pub first_block: Option<BlockId>,
490
    /// Map of entry points to block IDs.
491
    pub entry_points: HashMap<EntryPoint, Located<BlockId>>,
492
    /// Label information.
493
    pub labels: LabelMap,
494
}
495

496
impl Program {
497
    /// Creates an empty program.
498
    pub fn new() -> Self {
120✔
499
        Self::default()
120✔
500
    }
120✔
501

502
    /// Creates a program with blocks populated from `blocks`.
503
    pub fn with_blocks(blocks: impl Into<Vec<Block>>, first_block: Option<BlockId>) -> Self {
120✔
504
        Self { target: None, blocks: blocks.into(), first_block, ..Default::default() }
120✔
505
    }
120✔
506

507
    /// Inserts `block` after `after_id` in program order and returns the new block's ID.
508
    /// If `after_id` is `None`, the block will be inserted at the beginning of the program.
509
    pub fn insert_after(&mut self, after_id: Option<BlockId>, mut block: Block) -> BlockId {
93,405✔
510
        let new_id = BlockId::new(self.blocks.len() as u32);
93,405✔
511
        let next = match after_id {
93,405✔
512
            Some(id) => id.get(&self.blocks).next,
93,405✔
513
            None => self.first_block,
×
514
        };
515
        block.next = next;
93,405✔
516
        self.blocks.push(block);
93,405✔
517
        match after_id {
93,405✔
518
            Some(id) => id.get_mut(&mut self.blocks).next = Some(new_id),
93,405✔
519
            None => self.first_block = Some(new_id),
×
520
        };
521
        new_id
93,405✔
522
    }
93,405✔
523

524
    /// Scans through commands in the program and marks subroutines and entry points with the
525
    /// correct block flags.
526
    pub fn mark_subroutines(&mut self) {
240✔
527
        for index in 0..self.blocks.len() {
186,930✔
528
            // We can't get a mutable reference to self.blocks inside the operand loop, so
529
            // split it into (before, block, after) instead
530
            let (before, right) = self.blocks.split_at_mut(index);
186,930✔
531
            let (block, after) = right.split_first_mut().unwrap();
186,930✔
532
            if let Some(BlockContent::Code(code)) = &block.content {
186,810✔
533
                for cmd in code {
1,088,022✔
534
                    // Assume any top-level label reference operand which is not part of a control
535
                    // flow command refers to a subroutine
536
                    if !cmd.operands.is_empty() && !cmd.opcode.is_control_flow() {
904,770✔
537
                        for operand in &cmd.operands {
2,328,210✔
538
                            if let Operand::Label(label) = **operand {
1,660,452✔
539
                                let target_id = self.labels.get(label).block;
52,056✔
540
                                let target_index = target_id.index();
52,056✔
541
                                let flags = match target_index.cmp(&index) {
52,056✔
542
                                    Ordering::Less => &mut before[target_index].flags,
32,475✔
543
                                    Ordering::Equal => &mut block.flags,
18✔
544
                                    Ordering::Greater => &mut after[target_index - index - 1].flags,
19,563✔
545
                                };
546
                                flags.insert(BlockFlags::SUBROUTINE);
52,056✔
547
                            }
1,608,396✔
548
                        }
549
                    }
237,012✔
550
                }
551
            }
3,678✔
552
        }
553

554
        // Entry points are also subroutines
555
        for block_id in self.entry_points.values() {
13,104✔
556
            let block = block_id.get_mut(&mut self.blocks);
13,104✔
557
            block.flags.insert(BlockFlags::ENTRY_POINT | BlockFlags::SUBROUTINE);
13,104✔
558
        }
13,104✔
559
    }
240✔
560
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc