Skip to content

Commit 020798a

Browse files
Add support for target-specific custom enums in disassembler
This adds robust support for custom enums, allowing users to replace raw numeric values (immediate operands, data bytes, and data words) with semantic names across all supported assemblers (64tass, KickAssembler, ca65, and ACME) in a target-specific way. - 64tass formats enums using named dictionaries (e.g., Colors.BLACK) - KickAssembler formats enums inside .enum blocks (e.g., Colors.BLACK) - ca65 formats enums inside .enum blocks and double-colon scopes (e.g., Colors::BLACK) - ACME formats enums using flat underscore namespaces (e.g., Colors_BLACK) Precedence models shadow local > global > built-in, and enums are cloned and embedded permanently on use to guarantee portable, self-contained saves. Fixes #22 TAG=agy BUG=#22 CONV=168d6e0d-4484-483b-b051-42b81fefbdaf
1 parent b290a3a commit 020798a

37 files changed

Lines changed: 1899 additions & 160 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name = "VIC_Colors"
2+
3+
[variants]
4+
"0" = "BLACK"
5+
"1" = "WHITE"
6+
"2" = "RED"
7+
"3" = "CYAN"
8+
"4" = "PURPLE"
9+
"5" = "GREEN"
10+
"6" = "BLUE"
11+
"7" = "YELLOW"
12+
"8" = "ORANGE"
13+
"9" = "BROWN"
14+
"10" = "LIGHT_RED"
15+
"11" = "DARK_GREY"
16+
"12" = "GREY"
17+
"13" = "LIGHT_GREEN"
18+
"14" = "LIGHT_BLUE"
19+
"15" = "LIGHT_GREY"

crates/regenerator2000-core/src/assets.rs

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::state::{Label, LabelKind, LabelType};
1+
use crate::state::{EnumDefinition, Label, LabelKind, LabelType, RawEnumDefinition};
22
use anyhow::{Context, Result};
33
use directories::ProjectDirs;
44
use include_dir::{Dir, include_dir};
@@ -7,6 +7,7 @@ use std::collections::{BTreeMap, HashMap};
77
use std::path::{Path, PathBuf};
88

99
static SYSTEMS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets/systems");
10+
static ENUMS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets/enums");
1011

1112
#[derive(Debug, Clone, Serialize, Deserialize)]
1213
pub struct LabelOption {
@@ -362,6 +363,104 @@ pub fn load_excludes(system: &str) -> Vec<u16> {
362363
excludes
363364
}
364365

366+
// =============================================================================
367+
// Enum Assets Loading & Dumping
368+
// =============================================================================
369+
370+
/// Returns the path to the user's config directory for custom global enums.
371+
#[must_use]
372+
pub fn user_config_enums_dir() -> Option<PathBuf> {
373+
ProjectDirs::from("", "", "regenerator2000").map(|d| d.config_dir().to_path_buf())
374+
}
375+
376+
/// Load all built-in enums from the embedded `assets/enums/` directory.
377+
#[must_use]
378+
pub fn load_builtin_enums() -> BTreeMap<String, EnumDefinition> {
379+
let mut enums = BTreeMap::new();
380+
for file in ENUMS_DIR.files() {
381+
let Some(filename) = file.path().file_name().and_then(|s| s.to_str()) else {
382+
continue;
383+
};
384+
if !filename.starts_with("enum-") || !filename.ends_with(".toml") {
385+
continue;
386+
}
387+
let Some(content) = file.contents_utf8() else {
388+
continue;
389+
};
390+
match toml::from_str::<RawEnumDefinition>(content) {
391+
Ok(raw) => {
392+
let def = EnumDefinition::from(raw);
393+
enums.insert(def.name.clone(), def);
394+
}
395+
Err(e) => {
396+
log::warn!("Failed to parse built-in enum {filename}: {e}");
397+
}
398+
}
399+
}
400+
enums
401+
}
402+
403+
/// Load all custom global enums from the user's preferences directory.
404+
#[must_use]
405+
pub fn load_global_enums(dir: &Path) -> BTreeMap<String, EnumDefinition> {
406+
let mut enums = BTreeMap::new();
407+
let Ok(entries) = std::fs::read_dir(dir) else {
408+
return enums;
409+
};
410+
411+
for entry in entries.filter_map(Result::ok) {
412+
let path = entry.path();
413+
let Some(filename) = path.file_name().and_then(|s| s.to_str()) else {
414+
continue;
415+
};
416+
if !filename.starts_with("enum-") || !filename.ends_with(".toml") {
417+
continue;
418+
}
419+
match std::fs::read_to_string(&path) {
420+
Ok(content) => match toml::from_str::<RawEnumDefinition>(&content) {
421+
Ok(raw) => {
422+
let def = EnumDefinition::from(raw);
423+
log::info!("Loaded global enum: {} from {path:?}", def.name);
424+
enums.insert(def.name.clone(), def);
425+
}
426+
Err(e) => {
427+
log::warn!("Failed to parse enum file {path:?}: {e}");
428+
}
429+
},
430+
Err(e) => {
431+
log::warn!("Failed to read enum file {path:?}: {e}");
432+
}
433+
}
434+
}
435+
436+
enums
437+
}
438+
439+
/// Dump all embedded `enum-*.toml` files into `dest_dir`.
440+
///
441+
/// # Errors
442+
/// Returns an error if the directory cannot be created or if writing any file fails.
443+
pub fn dump_enum_files(dest_dir: &Path) -> Result<()> {
444+
std::fs::create_dir_all(dest_dir)
445+
.with_context(|| format!("Failed to create directory {dest_dir:?}"))?;
446+
447+
for file in ENUMS_DIR.files() {
448+
let Some(filename) = file.path().file_name() else {
449+
continue;
450+
};
451+
let filename_str = filename.to_string_lossy();
452+
if !filename_str.starts_with("enum-") || !filename_str.ends_with(".toml") {
453+
continue;
454+
}
455+
let dest_path = dest_dir.join(filename);
456+
std::fs::write(&dest_path, file.contents())
457+
.with_context(|| format!("Failed to write {dest_path:?}"))?;
458+
println!("Wrote {dest_path:?}");
459+
}
460+
461+
Ok(())
462+
}
463+
365464
#[cfg(test)]
366465
mod tests {
367466
use super::*;

crates/regenerator2000-core/src/commands.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::state::{Addr, AppState, BlockType, ImmediateFormat, Label};
1+
use crate::state::{Addr, AppState, BlockType, EnumDefinition, ImmediateFormat, Label};
22
use std::collections::BTreeMap;
33

44
#[derive(Debug, Clone)]
@@ -75,6 +75,16 @@ pub enum Command {
7575
old_labels: BTreeMap<Addr, Vec<crate::state::Label>>,
7676
old_cross_refs: BTreeMap<Addr, Vec<Addr>>,
7777
},
78+
SetEnumUsage {
79+
address: Addr,
80+
new_enum: Option<String>,
81+
old_enum: Option<String>,
82+
},
83+
SetEnumDefinition {
84+
name: String,
85+
new_definition: Option<EnumDefinition>,
86+
old_definition: Option<EnumDefinition>,
87+
},
7888
Batch(Vec<Command>),
7989
}
8090

@@ -225,6 +235,36 @@ impl Command {
225235
} => {
226236
state.scopes.remove(address);
227237
}
238+
Command::SetEnumUsage {
239+
address,
240+
new_enum,
241+
old_enum: _,
242+
} => {
243+
if let Some(enum_name) = new_enum {
244+
state.enum_usages.insert(*address, enum_name.clone());
245+
// Embed pre-defined global/built-in enum into local project enums if not present
246+
if !state.enums.contains_key(enum_name) {
247+
if let Some(def) = state.user_global_enums.get(enum_name) {
248+
state.enums.insert(enum_name.clone(), def.clone());
249+
} else if let Some(def) = state.builtin_enums.get(enum_name) {
250+
state.enums.insert(enum_name.clone(), def.clone());
251+
}
252+
}
253+
} else {
254+
state.enum_usages.remove(address);
255+
}
256+
}
257+
Command::SetEnumDefinition {
258+
name,
259+
new_definition,
260+
old_definition: _,
261+
} => {
262+
if let Some(def) = new_definition {
263+
state.enums.insert(name.clone(), def.clone());
264+
} else {
265+
state.enums.remove(name);
266+
}
267+
}
228268
Command::Batch(commands) => {
229269
for command in commands {
230270
command.apply(state);
@@ -381,6 +421,28 @@ impl Command {
381421
Command::RemoveScope { address, old_end } => {
382422
state.scopes.insert(*address, *old_end);
383423
}
424+
Command::SetEnumUsage {
425+
address,
426+
new_enum: _,
427+
old_enum,
428+
} => {
429+
if let Some(enum_name) = old_enum {
430+
state.enum_usages.insert(*address, enum_name.clone());
431+
} else {
432+
state.enum_usages.remove(address);
433+
}
434+
}
435+
Command::SetEnumDefinition {
436+
name,
437+
new_definition: _,
438+
old_definition,
439+
} => {
440+
if let Some(def) = old_definition {
441+
state.enums.insert(name.clone(), def.clone());
442+
} else {
443+
state.enums.remove(name);
444+
}
445+
}
384446
Command::Batch(commands) => {
385447
for command in commands.iter().rev() {
386448
command.undo(state);

crates/regenerator2000-core/src/core.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,9 @@ impl Core {
12001200
} => {
12011201
self.handle_apply_comment(address, text, kind, &mut events);
12021202
}
1203+
AppAction::ApplyEnumUsage { address, enum_name } => {
1204+
self.handle_apply_enum_usage(address, enum_name.as_deref(), &mut events);
1205+
}
12031206
AppAction::CyclePane => {
12041207
use crate::view_state::ActivePane;
12051208
self.view.active_pane = match self.view.active_pane {
@@ -2286,6 +2289,36 @@ impl Core {
22862289
events.push(CoreEvent::DialogDismissalRequested);
22872290
}
22882291

2292+
fn handle_apply_enum_usage(
2293+
&mut self,
2294+
address: Addr,
2295+
enum_name: Option<&str>,
2296+
events: &mut Vec<CoreEvent>,
2297+
) {
2298+
let old_enum = self.state.enum_usages.get(&address).cloned();
2299+
let new_enum = enum_name.map(String::from);
2300+
2301+
let command = crate::commands::Command::SetEnumUsage {
2302+
address,
2303+
new_enum: new_enum.clone(),
2304+
old_enum,
2305+
};
2306+
2307+
command.apply(&mut self.state);
2308+
self.state.push_command(command);
2309+
2310+
events.push(CoreEvent::StatusMessage(if new_enum.is_none() {
2311+
"Enum removed".to_string()
2312+
} else {
2313+
format!("Enum '{}' applied", new_enum.unwrap_or_default())
2314+
}));
2315+
2316+
self.state.disassemble();
2317+
events.push(CoreEvent::StateChanged);
2318+
events.push(CoreEvent::ViewChanged);
2319+
events.push(CoreEvent::DialogDismissalRequested);
2320+
}
2321+
22892322
fn handle_apply_comment(
22902323
&mut self,
22912324
address: Addr,

crates/regenerator2000-core/src/disassembler.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ impl Disassembler {
320320
splitters: &BTreeSet<Addr>,
321321
scopes: &BTreeMap<Addr, Addr>,
322322
) -> Vec<DisassemblyLine> {
323+
let empty_enums = BTreeMap::new();
324+
let empty_usages = BTreeMap::new();
323325
let ctx = DisassemblyContext {
324326
data,
325327
block_types,
@@ -334,6 +336,10 @@ impl Disassembler {
334336
collapsed_blocks,
335337
splitters,
336338
scopes,
339+
enums: &empty_enums,
340+
enum_usages: &empty_usages,
341+
user_global_enums: &empty_enums,
342+
builtin_enums: &empty_enums,
337343
};
338344
self.disassemble_ctx(&ctx)
339345
}
@@ -873,6 +879,10 @@ impl Disassembler {
873879
current_scope_name: current_scope_name.as_deref(),
874880
scope_separator: formatter.scope_resolution_separator(),
875881
local_prefix: formatter.local_label_prefix(),
882+
enums: ctx.enums,
883+
enum_usages: ctx.enum_usages,
884+
user_global_enums: ctx.user_global_enums,
885+
builtin_enums: ctx.builtin_enums,
876886
};
877887
let (mnemonic, operand_str) = formatter.format_instruction(&ctx);
878888

@@ -1087,7 +1097,14 @@ impl Disassembler {
10871097

10881098
let b = data[current_pc];
10891099
bytes.push(b);
1090-
operands.push(formatter.format_byte(b));
1100+
let formatted_val = if let Some((enum_name, variant_name)) =
1101+
ctx.resolve_enum_value(current_address, b as u16)
1102+
{
1103+
formatter.format_enum_reference(&enum_name, &variant_name)
1104+
} else {
1105+
formatter.format_byte(b)
1106+
};
1107+
operands.push(formatted_val);
10911108
count += 1;
10921109
}
10931110

@@ -1179,7 +1196,14 @@ impl Disassembler {
11791196

11801197
bytes.push(low);
11811198
bytes.push(high);
1182-
operands.push(formatter.format_address(Addr(val)));
1199+
let formatted_val = if let Some((enum_name, variant_name)) =
1200+
ctx.resolve_enum_value(current_address, val)
1201+
{
1202+
formatter.format_enum_reference(&enum_name, &variant_name)
1203+
} else {
1204+
formatter.format_address(Addr(val))
1205+
};
1206+
operands.push(formatted_val);
11831207
count += 1;
11841208
}
11851209

crates/regenerator2000-core/src/disassembler/context.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::state::{Addr, BlockType, DocumentSettings, Label};
1+
use crate::state::{Addr, BlockType, DocumentSettings, EnumDefinition, Label};
22
use std::collections::{BTreeMap, BTreeSet};
33

44
use super::formatter::Formatter;
@@ -18,6 +18,12 @@ pub struct DisassemblyContext<'a> {
1818
pub collapsed_blocks: &'a [(usize, usize)],
1919
pub splitters: &'a BTreeSet<Addr>,
2020
pub scopes: &'a BTreeMap<Addr, Addr>,
21+
22+
// Enums references
23+
pub enums: &'a BTreeMap<String, EnumDefinition>,
24+
pub enum_usages: &'a BTreeMap<Addr, String>,
25+
pub user_global_enums: &'a BTreeMap<String, EnumDefinition>,
26+
pub builtin_enums: &'a BTreeMap<String, EnumDefinition>,
2127
}
2228

2329
/// Per-iteration values computed in the disassembly loop and passed to each handler.
@@ -50,6 +56,10 @@ impl<'a> DisassemblyContext<'a> {
5056
collapsed_blocks: &'a [(usize, usize)],
5157
splitters: &'a BTreeSet<Addr>,
5258
scopes: &'a BTreeMap<Addr, Addr>,
59+
enums: &'a BTreeMap<String, EnumDefinition>,
60+
enum_usages: &'a BTreeMap<Addr, String>,
61+
user_global_enums: &'a BTreeMap<String, EnumDefinition>,
62+
builtin_enums: &'a BTreeMap<String, EnumDefinition>,
5363
) -> Self {
5464
Self {
5565
data,
@@ -65,9 +75,25 @@ impl<'a> DisassemblyContext<'a> {
6575
collapsed_blocks,
6676
splitters,
6777
scopes,
78+
enums,
79+
enum_usages,
80+
user_global_enums,
81+
builtin_enums,
6882
}
6983
}
7084

85+
#[must_use]
86+
pub fn resolve_enum_value(&self, address: Addr, value: u16) -> Option<(String, String)> {
87+
let enum_name = self.enum_usages.get(&address)?;
88+
let enum_def = self
89+
.enums
90+
.get(enum_name)
91+
.or_else(|| self.user_global_enums.get(enum_name))
92+
.or_else(|| self.builtin_enums.get(enum_name))?;
93+
let variant = enum_def.variants.get(&value)?;
94+
Some((enum_name.clone(), variant.clone()))
95+
}
96+
7197
#[must_use]
7298
pub fn is_virtual_splitter(&self, addr: Addr) -> bool {
7399
if self.splitters.contains(&addr) {

0 commit comments

Comments
 (0)