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

adierking / unplug / 25709640189

12 May 2026 02:29AM UTC coverage: 76.993% (-0.008%) from 77.001%
25709640189

push

github

adierking
cli: Dry run mode for script assembly

Useful for testing that something compiles without updating any game files.

5 of 25 new or added lines in 2 files covered. (20.0%)

19035 of 24723 relevant lines covered (76.99%)

1067798.44 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)?;
×
30
    let info = ctx.query_file(&file)?;
×
31

32
    info!("Reading script globals");
×
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(())
×
41
}
×
42

43
pub fn command_disassemble_all(ctx: Context, args: DisassembleAllArgs) -> Result<()> {
×
44
    let mut ctx = ctx.open_read()?;
×
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)?;
×
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())?;
×
60
        asm::write_program(&program, writer)?;
×
61
    }
62
    Ok(())
×
63
}
×
64

65
/// Reports diagnostics from a compilation stage.
66
fn report_diagnostics<'f, F>(file: &'f F, diagnostics: &mut [Diagnostic])
×
67
where
×
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()));
×
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),
×
100
                    _ => ReportLabel::secondary((), range),
×
101
                };
102
                if let Some(tag) = l.tag() {
×
103
                    label = label.with_message(tag);
×
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(),
×
121
        0 => "".to_owned(),
×
122
    };
123
    match (warnings, errors) {
×
124
        (0, 0) => (),
×
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>,
×
137
) -> Result<T>
×
138
where
×
139
    F: Files<'f, FileId = ()>,
×
140
{
141
    if !output.diagnostics.is_empty() {
×
142
        diagnostics.append(&mut output.diagnostics);
×
143
    }
×
144
    output.result.ok_or_else(|| {
×
145
        report_diagnostics(file, diagnostics);
×
146
        anyhow!("script assembly failed")
×
147
    })
×
148
}
×
149

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

154
    let name = args.path.file_name().unwrap_or_default().to_string_lossy();
×
155
    info!("Parsing {}", name);
×
156
    let source = fs::read_to_string(&args.path)?;
×
157
    let file = SimpleFile::new(name, &source);
×
158
    let lexer = Lexer::new(&source);
×
159
    let parser = Parser::new(lexer);
×
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())?;
×
165
    let compiled = check_output(&file, &mut diagnostics, asm::compile(&program))?;
×
166
    if !diagnostics.is_empty() {
×
167
        // Print warnings.
×
168
        report_diagnostics(&file, &mut diagnostics);
×
169
    }
×
NEW
170
    if compiled.target.is_none() {
×
NEW
171
        bail!("The script does not have a .globals or .stage directive");
×
NEW
172
    }
×
173

NEW
174
    if let Some(ctx) = ctx.as_mut() {
×
NEW
175
        let update = match compiled.target.as_ref().unwrap() {
×
176
            Target::Globals => {
NEW
177
                let libs = compiled.into_libs()?;
×
NEW
178
                let mut globals = ctx.read_globals()?;
×
NEW
179
                ctx.begin_update()
×
NEW
180
                    .write_globals(GlobalsBuilder::new().base(&mut globals).libs(&libs))?
×
181
            }
NEW
182
            Target::Stage(stage_name) => {
×
NEW
183
                let stage_id = StageId::find(stage_name)
×
NEW
184
                    .ok_or_else(|| anyhow!("Unknown stage \"{stage_name}\""))?;
×
NEW
185
                let libs = ctx.read_globals()?.read_libs()?;
×
NEW
186
                let mut stage = ctx.read_stage(&libs, stage_id)?;
×
NEW
187
                stage = compiled.into_stage(stage)?;
×
NEW
188
                ctx.begin_update().write_stage(stage_id, &stage)?
×
189
            }
190
        };
NEW
191
        info!("Updating game files");
×
NEW
192
        update.commit()?;
×
193
    } else {
NEW
194
        info!("Dry run: no game files updated");
×
195
    }
196
    Ok(())
×
197
}
×
198

199
/// The `script` CLI command.
200
pub fn command(ctx: Context, command: Subcommand) -> Result<()> {
×
201
    match command {
×
202
        Subcommand::Disassemble(args) => command_disassemble(ctx, args),
×
203
        Subcommand::DisassembleAll(args) => command_disassemble_all(ctx, args),
×
204
        Subcommand::Assemble(args) => command_assemble(ctx, args),
×
205
    }
206
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc