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

adierking / unplug / 25621452374

10 May 2026 06:05AM UTC coverage: 77.135% (-0.1%) from 77.259%
25621452374

push

github

adierking
asm: Always sign-extend expression values

I wasn't satisfied with the previous change sometimes emitting `Imm32` for a
value with a `.w` suffix. So, instead, always sign-extend values with explicit
sizes in expressions:

- `255.b` becomes `-1.b`
- `65535.w` becomes `-1.w`
- `255` (no suffix) becomes `255.w`
- `65535` (no suffix) becomes `65535.d`

If sign extension occurs, the user will be given a warning to drop the type
suffix because they probably didn't intend this.

Since the disassembler always reads expression values as signed, this will not
affect the assembly of the vanilla game scripts, however this may be a breaking
change for user code.

13 of 25 new or added lines in 2 files covered. (52.0%)

71 existing lines in 3 files now uncovered.

19023 of 24662 relevant lines covered (77.13%)

1070285.23 hits per line

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

0.0
/unplug-cli/src/commands/script.rs
1
use crate::args::script::*;
2

3
use crate::common::find_stage_file;
4
use crate::context::Context;
5
use anyhow::{anyhow, bail, Result};
6
use asm::diagnostics::{CompileOutput, Diagnostic};
7
use codespan_reporting::diagnostic::{Diagnostic as ReportDiagnostic, Label as ReportLabel};
8
use codespan_reporting::files::{Files, SimpleFile};
9
use codespan_reporting::term;
10
use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
11
use log::{error, info, warn};
12
use std::fs::{self, File};
13
use std::io::BufWriter;
14
use std::ops::Range;
15
use std::path::Path;
16
use unplug::data::{Resource, Stage as StageId};
17
use unplug::globals::GlobalsBuilder;
18
use unplug_asm as asm;
19
use unplug_asm::assembler::ProgramAssembler;
20
use unplug_asm::diagnostics::DiagnosticCode;
21
use unplug_asm::lexer::Lexer;
22
use unplug_asm::parser::Parser;
23
use unplug_asm::program::Target;
24
use unplug_asm::span::Spanned;
25

26
fn command_disassemble(ctx: Context, args: DisassembleArgs) -> Result<()> {
×
27
    let mut ctx = ctx.open_read()?;
×
28
    let out = BufWriter::new(File::create(args.output)?);
×
29
    let file = find_stage_file(&mut ctx, &args.stage)?;
×
UNCOV
30
    let info = ctx.query_file(&file)?;
×
31

32
    info!("Reading script globals");
×
UNCOV
33
    let libs = ctx.read_globals()?.read_libs()?;
×
34

35
    info!("Disassembling {}", ctx.query_file(&file)?.name);
×
36
    let stage = ctx.read_stage_file(&libs, &file)?;
×
37
    let name = info.name.rsplit_once('.').unwrap_or((&info.name, "")).0;
×
38
    let program = asm::disassemble_stage(&stage, name)?;
×
39
    asm::write_program(&program, out)?;
×
40
    Ok(())
×
UNCOV
41
}
×
42

43
pub fn command_disassemble_all(ctx: Context, args: DisassembleAllArgs) -> Result<()> {
×
44
    let mut ctx = ctx.open_read()?;
×
UNCOV
45
    fs::create_dir_all(&args.output)?;
×
46

47
    info!("Disassembling script globals");
×
48
    let libs = ctx.read_globals()?.read_libs()?;
×
49
    let libs_out = Path::join(&args.output, "globals.us");
×
50
    let libs_writer = BufWriter::new(File::create(libs_out)?);
×
51
    let libs_program = asm::disassemble_globals(&libs)?;
×
UNCOV
52
    asm::write_program(&libs_program, libs_writer)?;
×
53

54
    for id in StageId::iter() {
×
55
        info!("Disassembling {}", id.file_name());
×
56
        let stage = ctx.read_stage(&libs, id)?;
×
57
        let out_path = Path::join(&args.output, format!("{}.us", id.name()));
×
58
        let writer = BufWriter::new(File::create(out_path)?);
×
59
        let program = asm::disassemble_stage(&stage, id.name())?;
×
UNCOV
60
        asm::write_program(&program, writer)?;
×
61
    }
62
    Ok(())
×
UNCOV
63
}
×
64

65
/// Reports diagnostics from a compilation stage.
66
fn report_diagnostics<'f, F>(file: &'f F, diagnostics: &mut [Diagnostic])
×
67
where
×
UNCOV
68
    F: Files<'f, FileId = ()>,
×
69
{
70
    diagnostics.sort_by_key(Diagnostic::span);
×
71
    let writer = StandardStream::stderr(ColorChoice::Auto);
×
72
    let config = term::Config::default();
×
73
    let mut lock = writer.lock();
×
74
    let mut warnings: usize = 0;
×
75
    let mut errors: usize = 0;
×
76
    for diagnostic in diagnostics {
×
77
        let mut report = match diagnostic.code() {
×
78
            DiagnosticCode::Warning(_) => {
79
                warnings += 1;
×
80
                ReportDiagnostic::warning()
×
81
            }
82
            DiagnosticCode::Error(_) => {
83
                errors += 1;
×
84
                ReportDiagnostic::error()
×
85
            }
86
        };
87
        report =
×
88
            report.with_message(diagnostic.message()).with_code(format!("{}", diagnostic.code()));
×
UNCOV
89
        if let Some(note) = diagnostic.note() {
×
90
            report = report.with_notes(vec![note.to_owned()]);
×
91
        }
×
92
        let labels = diagnostic
×
93
            .labels()
×
94
            .iter()
×
95
            .enumerate()
×
96
            .map(|(i, l)| {
×
97
                let range = Range::<usize>::try_from(l.span()).unwrap();
×
98
                let mut label = match i {
×
99
                    0 => ReportLabel::primary((), range),
×
UNCOV
100
                    _ => ReportLabel::secondary((), range),
×
101
                };
UNCOV
102
                if let Some(tag) = l.tag() {
×
UNCOV
103
                    label = label.with_message(tag);
×
UNCOV
104
                }
×
105
                label
×
106
            })
×
107
            .collect::<Vec<_>>();
×
108
        if !labels.is_empty() {
×
109
            report = report.with_labels(labels);
×
110
        }
×
111
        term::emit(&mut lock, &config, file, &report).unwrap();
×
112
    }
113
    let warnings_str = match warnings {
×
114
        2.. => format!("{warnings} warnings"),
×
115
        1 => "1 warning".to_owned(),
×
116
        0 => "".to_owned(),
×
117
    };
118
    let errors_str = match errors {
×
119
        2.. => format!("{errors} errors"),
×
120
        1 => "1 error".to_owned(),
×
UNCOV
121
        0 => "".to_owned(),
×
122
    };
123
    match (warnings, errors) {
×
UNCOV
124
        (0, 0) => (),
×
UNCOV
125
        (0, _) => error!("{errors_str} found"),
×
126
        (_, 0) => warn!("{warnings_str} found"),
×
127
        (_, _) => error!("{warnings_str} and {errors_str} found"),
×
128
    }
129
}
×
130

131
/// Checks the output of a compilation stage and pools diagnostics into a single list. If a result is available,
132
/// the result value will be returned, otherwise this will report all diagnostics and fail.
133
fn check_output<'f, F, T>(
×
134
    file: &'f F,
×
135
    diagnostics: &mut Vec<Diagnostic>,
×
136
    mut output: CompileOutput<T>,
×
UNCOV
137
) -> Result<T>
×
138
where
×
139
    F: Files<'f, FileId = ()>,
×
140
{
141
    if !output.diagnostics.is_empty() {
×
UNCOV
142
        diagnostics.append(&mut output.diagnostics);
×
143
    }
×
144
    output.result.ok_or_else(|| {
×
145
        report_diagnostics(file, diagnostics);
×
146
        anyhow!("script assembly failed")
×
UNCOV
147
    })
×
148
}
×
149

150
/// The `script assemble` CLI command.
151
fn command_assemble(ctx: Context, args: AssembleArgs) -> Result<()> {
×
152
    let mut ctx = ctx.open_read_write()?;
×
153

154
    let name = args.path.file_name().unwrap_or_default().to_string_lossy();
×
UNCOV
155
    info!("Parsing {}", name);
×
UNCOV
156
    let source = fs::read_to_string(&args.path)?;
×
157
    let file = SimpleFile::new(name, &source);
×
UNCOV
158
    let lexer = Lexer::new(&source);
×
UNCOV
159
    let parser = Parser::new(lexer);
×
UNCOV
160
    let mut diagnostics = vec![];
×
161
    let ast = check_output(&file, &mut diagnostics, parser.parse())?;
×
162

163
    info!("Assembling script");
×
164
    let program = check_output(&file, &mut diagnostics, ProgramAssembler::new(&ast).assemble())?;
×
UNCOV
165
    let compiled = check_output(&file, &mut diagnostics, asm::compile(&program))?;
×
UNCOV
166
    if !diagnostics.is_empty() {
×
167
        // Print warnings.
×
168
        report_diagnostics(&file, &mut diagnostics);
×
169
    }
×
170
    let update = match &compiled.target {
×
171
        Some(Target::Globals) => {
UNCOV
172
            let libs = compiled.into_libs()?;
×
173
            let mut globals = ctx.read_globals()?;
×
UNCOV
174
            ctx.begin_update()
×
UNCOV
175
                .write_globals(GlobalsBuilder::new().base(&mut globals).libs(&libs))?
×
176
        }
UNCOV
177
        Some(Target::Stage(stage_name)) => {
×
UNCOV
178
            let stage_id = StageId::find(stage_name)
×
UNCOV
179
                .ok_or_else(|| anyhow!("Unknown stage \"{stage_name}\""))?;
×
UNCOV
180
            let libs = ctx.read_globals()?.read_libs()?;
×
UNCOV
181
            let mut stage = ctx.read_stage(&libs, stage_id)?;
×
UNCOV
182
            stage = compiled.into_stage(stage)?;
×
UNCOV
183
            ctx.begin_update().write_stage(stage_id, &stage)?
×
184
        }
185
        None => {
UNCOV
186
            bail!("The script does not have a .globals or .stage directive");
×
187
        }
188
    };
189

UNCOV
190
    info!("Updating game files");
×
UNCOV
191
    update.commit()?;
×
UNCOV
192
    Ok(())
×
UNCOV
193
}
×
194

195
/// The `script` CLI command.
UNCOV
196
pub fn command(ctx: Context, command: Subcommand) -> Result<()> {
×
UNCOV
197
    match command {
×
UNCOV
198
        Subcommand::Disassemble(args) => command_disassemble(ctx, args),
×
UNCOV
199
        Subcommand::DisassembleAll(args) => command_disassemble_all(ctx, args),
×
UNCOV
200
        Subcommand::Assemble(args) => command_assemble(ctx, args),
×
201
    }
UNCOV
202
}
×
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