Skip to content

Commit 91ce99d

Browse files
authored
fix(cli): validate artifact arguments and implement object debug output (#292)
Signed-off-by: LunaStev <luna@lunastev.org>
1 parent 06577d6 commit 91ce99d

7 files changed

Lines changed: 325 additions & 82 deletions

File tree

‎README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,30 @@ cargo run --release --locked -- asm --amd64 example.asm -o example.o
108108
This produces a relocatable ELF64 object. Assembly is implemented within Whale;
109109
no external assembler is needed.
110110

111+
The `asm` and `object` commands reject unknown options, extra input paths,
112+
duplicate `-o` options, and missing output paths before reading input or writing
113+
output. Specify the assembly architecture once. For a path beginning with `-`,
114+
use a relative `./` prefix or an absolute path.
115+
116+
### Wrap raw bytes in an object
117+
118+
```sh
119+
cargo run --locked -- object raw.bin -o raw.o
120+
cargo run --locked -- object --help
121+
```
122+
123+
This places the input bytes in an AMD64 ELF64 `.text` section with a global
124+
`start` symbol. The input must already contain the intended machine code.
125+
126+
Both `asm` and `object` support `--debug-whale` with `--bytes`, `--dump-hex`,
127+
`--dump-bin`, or `--dump-json` to inspect the first 256 bytes of the emitted
128+
object. JSON includes the full object length and a bounded byte array.
129+
For example:
130+
131+
```sh
132+
cargo run --locked -- object raw.bin -o raw.o --debug-whale --dump-hex
133+
```
134+
111135
### Execute scalar integer IR
112136

113137
Save this format 4 module as `answer.wir`:

‎src/cli.rs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub fn run() {
1111
};
1212

1313
match cmd.as_str() {
14+
"--help" | "-h" => print_help(),
1415
"asm" => commands::asm::run(args.collect()),
1516
"object" => commands::object::run(args.collect()),
1617
"link" => commands::linker::run(args.collect()),
@@ -25,14 +26,14 @@ pub fn run() {
2526

2627
fn print_help() {
2728
println!("Usage:");
28-
println!(" whale asm [--amd64 | --aarch64] <input> -o <output>");
29+
println!(" whale asm --amd64 <input> -o <output.o>");
2930
println!(" whale object <input> -o <output>");
3031
println!(" whale link <...>");
3132
println!(" whale ir <subcommand> [options]");
3233
println!();
3334
println!("Commands:");
34-
println!(" asm Assemble source file");
35-
println!(" object Generate object file from binary or IR");
36-
println!(" link Link object files into an executable");
37-
println!(" ir IR tools (verify/print; socket-cli enables AST lower)");
35+
println!(" asm Assemble AMD64 source into an ELF64 object");
36+
println!(" object Wrap raw bytes in an AMD64 ELF64 object");
37+
println!(" link Executable linking (not implemented)");
38+
println!(" ir IR tools (verify/print/run; socket-cli enables AST lower)");
3839
}

‎src/commands/asm.rs‎

Lines changed: 34 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::fs;
22
use std::process;
33
use std::time::Instant;
44

5+
use super::debug::dump_bytes;
6+
57
use assembler::isa::amd64::parser::parse;
68
use assembler::tokens::tokenize;
79
use assembler::{assemble, isa::AMD64, AssemblerOutput};
@@ -41,14 +43,32 @@ pub fn run(args: Vec<String>) {
4143
return;
4244
}
4345

44-
"--amd64" => arch = Some("amd64"),
45-
"--aarch64" => arch = Some("aarch64"),
46+
flag @ ("--amd64" | "--aarch64") => {
47+
if arch.is_some() {
48+
eprintln!("Error: architecture must be specified only once");
49+
process::exit(1);
50+
}
51+
arch = Some(if flag == "--amd64" {
52+
"amd64"
53+
} else {
54+
"aarch64"
55+
});
56+
}
4657

4758
"-o" => {
48-
if i + 1 < args.len() {
49-
output = Some(args[i + 1].clone());
50-
i += 1;
59+
if output.is_some() {
60+
eprintln!("Error: duplicate -o option");
61+
process::exit(1);
5162
}
63+
let path = args
64+
.get(i + 1)
65+
.filter(|s| !s.starts_with('-'))
66+
.unwrap_or_else(|| {
67+
eprintln!("Error: -o requires a path (use ./ for paths starting with '-')");
68+
process::exit(1);
69+
});
70+
output = Some(path.clone());
71+
i += 1;
5272
}
5373

5474
"--debug-whale" => debug_mode = true,
@@ -64,7 +84,15 @@ pub fn run(args: Vec<String>) {
6484
"--trace" => trace_enable = true,
6585

6686
s if input.is_none() && !s.starts_with('-') => input = Some(s.to_string()),
67-
_ => {}
87+
s => {
88+
let kind = if s.starts_with('-') {
89+
"unknown option"
90+
} else {
91+
"unexpected argument"
92+
};
93+
eprintln!("Error: {kind}: {s}");
94+
process::exit(1);
95+
}
6896
}
6997
i += 1;
7098
}
@@ -273,64 +301,6 @@ fn build_elf_from_asm_output(out: &AssemblerOutput) -> Result<Vec<u8>, String> {
273301
obj.write()
274302
}
275303

276-
fn dump_bytes(
277-
label: &str,
278-
bytes: &[u8],
279-
show_bytes: bool,
280-
dump_hex: bool,
281-
dump_bin: bool,
282-
dump_json: bool,
283-
) {
284-
const LIMIT: usize = 256;
285-
let n = bytes.len().min(LIMIT);
286-
let head = &bytes[..n];
287-
288-
if show_bytes {
289-
println!(
290-
"== BYTES ({}, {} bytes, head {} bytes) ==",
291-
label,
292-
bytes.len(),
293-
n
294-
);
295-
for (i, b) in head.iter().enumerate() {
296-
println!("{:04X}: {}", i, b);
297-
}
298-
}
299-
300-
if dump_hex {
301-
println!("== HEX ({}, head {} bytes) ==", label, n);
302-
for (i, chunk) in head.chunks(16).enumerate() {
303-
print!("{:04X}: ", i * 16);
304-
for b in chunk {
305-
print!("{:02X} ", b);
306-
}
307-
println!();
308-
}
309-
}
310-
311-
if dump_bin {
312-
println!("== BIN ({}, head {} bytes) ==", label, n);
313-
for (i, b) in head.iter().enumerate() {
314-
println!("{:04X}: {:08b}", i, b);
315-
}
316-
}
317-
318-
if dump_json {
319-
println!("== JSON ({}) ==", label);
320-
println!("{{");
321-
println!(" \"len\": {},", bytes.len());
322-
print!(" \"head\": [");
323-
for (i, b) in head.iter().enumerate() {
324-
if i != 0 {
325-
print!(", ");
326-
}
327-
print!("{}", b);
328-
}
329-
println!("]");
330-
println!("}}");
331-
}
332-
}
333-
334304
fn print_help() {
335305
println!("Usage:");
336306
println!(" whale asm --amd64 <input> -o <output.o>");

‎src/commands/debug.rs‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
pub(super) fn dump_bytes(
2+
label: &str,
3+
bytes: &[u8],
4+
show_bytes: bool,
5+
dump_hex: bool,
6+
dump_bin: bool,
7+
dump_json: bool,
8+
) {
9+
const LIMIT: usize = 256;
10+
let n = bytes.len().min(LIMIT);
11+
let head = &bytes[..n];
12+
13+
if show_bytes {
14+
println!(
15+
"== BYTES ({}, {} bytes, head {} bytes) ==",
16+
label,
17+
bytes.len(),
18+
n
19+
);
20+
for (i, b) in head.iter().enumerate() {
21+
println!("{:04X}: {}", i, b);
22+
}
23+
}
24+
25+
if dump_hex {
26+
println!("== HEX ({}, head {} bytes) ==", label, n);
27+
for (i, chunk) in head.chunks(16).enumerate() {
28+
print!("{:04X}: ", i * 16);
29+
for b in chunk {
30+
print!("{:02X} ", b);
31+
}
32+
println!();
33+
}
34+
}
35+
36+
if dump_bin {
37+
println!("== BIN ({}, head {} bytes) ==", label, n);
38+
for (i, b) in head.iter().enumerate() {
39+
println!("{:04X}: {:08b}", i, b);
40+
}
41+
}
42+
43+
if dump_json {
44+
println!("== JSON ({}) ==", label);
45+
println!("{{");
46+
println!(" \"len\": {},", bytes.len());
47+
print!(" \"head\": [");
48+
for (i, b) in head.iter().enumerate() {
49+
if i != 0 {
50+
print!(", ");
51+
}
52+
print!("{}", b);
53+
}
54+
println!("]");
55+
println!("}}");
56+
}
57+
}

‎src/commands/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod asm;
2+
mod debug;
23
pub mod linker;
34
pub mod object;
45
mod output;

‎src/commands/object.rs‎

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use std::fs;
33
use std::process;
44
use std::time::Instant;
55

6+
use super::debug::dump_bytes;
7+
68
pub fn run(args: Vec<String>) {
79
if args.is_empty() {
810
print_help();
@@ -13,14 +15,10 @@ pub fn run(args: Vec<String>) {
1315
let mut output = None;
1416

1517
let mut debug_mode = false;
16-
let mut _show_ast = false;
17-
let mut _show_token = false;
1818
let mut show_bytes = false;
1919
let mut dump_hex = false;
2020
let mut dump_bin = false;
21-
let mut _dump_json = false;
22-
let mut _no_color = false;
23-
let mut _no_warn_ext = false;
21+
let mut dump_json = false;
2422
let mut show_stats = false;
2523
let mut trace_enable = false;
2624

@@ -32,20 +30,37 @@ pub fn run(args: Vec<String>) {
3230
return;
3331
}
3432
"-o" => {
35-
if i + 1 < args.len() {
36-
output = Some(args[i + 1].clone());
37-
i += 1;
33+
if output.is_some() {
34+
eprintln!("Error: duplicate -o option");
35+
process::exit(1);
3836
}
37+
let path = args
38+
.get(i + 1)
39+
.filter(|s| !s.starts_with('-'))
40+
.unwrap_or_else(|| {
41+
eprintln!("Error: -o requires a path (use ./ for paths starting with '-')");
42+
process::exit(1);
43+
});
44+
output = Some(path.clone());
45+
i += 1;
3946
}
4047
"--debug-whale" => debug_mode = true,
4148
"--bytes" => show_bytes = true,
4249
"--dump-hex" => dump_hex = true,
4350
"--dump-bin" => dump_bin = true,
44-
"--dump-json" => _dump_json = true,
51+
"--dump-json" => dump_json = true,
4552
"--stats" => show_stats = true,
4653
"--trace" => trace_enable = true,
4754
s if input.is_none() && !s.starts_with('-') => input = Some(s.to_string()),
48-
_ => {}
55+
s => {
56+
let kind = if s.starts_with('-') {
57+
"unknown option"
58+
} else {
59+
"unexpected argument"
60+
};
61+
eprintln!("Error: {kind}: {s}");
62+
process::exit(1);
63+
}
4964
}
5065
i += 1;
5166
}
@@ -88,13 +103,17 @@ pub fn run(args: Vec<String>) {
88103
});
89104
let elapsed = start_time.elapsed();
90105

91-
if debug_mode && (show_bytes || dump_hex || dump_bin) { /* ... same as before ... */ }
92-
93106
super::output::publish(input.as_ref(), output.as_ref(), &out_bytes).unwrap_or_else(|e| {
94107
eprintln!("Failed to write {output}: {e}");
95108
process::exit(1);
96109
});
97110

111+
if debug_mode && (show_bytes || dump_hex || dump_bin || dump_json) {
112+
dump_bytes(
113+
"object", &out_bytes, show_bytes, dump_hex, dump_bin, dump_json,
114+
);
115+
}
116+
98117
if debug_mode && show_stats {
99118
println!(
100119
"== STATS ==\nInput bytes: {}\nOutput bytes: {}\nTime: {} ms",
@@ -106,5 +125,19 @@ pub fn run(args: Vec<String>) {
106125
println!("Created object file {} ({} bytes)", output, out_bytes.len());
107126
}
108127

109-
pub fn print_help() { /* ... same as before ... */
128+
pub fn print_help() {
129+
println!("Usage:");
130+
println!(" whale object <input> -o <output>");
131+
println!();
132+
println!("Wrap raw bytes in an AMD64 ELF64 object with a .text section.");
133+
println!();
134+
println!("Options:");
135+
println!(" --help show this help");
136+
println!(" --debug-whale enable debug features");
137+
println!(" --bytes print object bytes (debug, first 256 bytes)");
138+
println!(" --dump-hex print object bytes as hex (debug, first 256 bytes)");
139+
println!(" --dump-bin print object bytes as binary (debug, first 256 bytes)");
140+
println!(" --dump-json print object size and byte head as JSON (debug)");
141+
println!(" --stats show stats (debug)");
142+
println!(" --trace trace logs");
110143
}

0 commit comments

Comments
 (0)