snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
848 lines • 37.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SNESDisassembler = void 0;
const decoder_1 = require("./decoder");
const rom_header_parser_1 = require("./rom-header-parser");
const rom_parser_1 = require("./rom-parser");
const analysis_engine_1 = require("./analysis-engine");
const output_formats_extended_1 = require("./output-formats-extended");
const symbol_manager_1 = require("./symbol-manager");
const validation_engine_1 = require("./validation-engine");
const snes_reference_tables_1 = require("./snes-reference-tables");
const spc_exporter_1 = require("./spc-exporter");
const spc_state_extractor_1 = require("./spc-state-extractor");
const analysis_cache_1 = require("./analysis-cache");
const logger_1 = require("./utils/logger");
class SNESDisassembler {
constructor(romPath, options = {}) {
this.lastAnalysisHash = '';
this.isAnalyzing = false;
this.rom = rom_parser_1.RomParser.parse(romPath);
// Initialize logger for this disassembler instance
this.logger = (0, logger_1.createLogger)('SNESDisassembler');
// Initialize enhanced ROM parsing for bank switching
const mappingMode = rom_header_parser_1.RomHeaderParser.detectMappingMode(this.rom.cartridgeInfo);
// Log detected mapping mode
this.logger.info(`Detected Mapping Mode: ${mappingMode}`);
this.decoder = new decoder_1.InstructionDecoder();
this.labels = options.labels || new Map();
this.comments = options.comments || new Map();
this.analysisEngine = new analysis_engine_1.AnalysisEngine();
this.symbolManager = new symbol_manager_1.SymbolManager();
this.validationEngine = new validation_engine_1.SNESValidationEngine(options.validationLogLevel || 'normal');
this.enableValidation = options.enableValidation !== false; // Default to true
this.enhanceComments = options.enhanceComments !== false; // Default to true
this.cache = options.cache || analysis_cache_1.globalROMCache;
// Cache ROM info for reuse
this.cache.setROMInfo(this.rom);
}
// Returns ROM information without using analysis cache to prevent circular dependencies
// This method is called during cache key generation and initialization phases
getRomInfo() {
return this.rom;
}
disassemble(startAddress, endAddress) {
// Default to reset vector if no start address provided
let currentAddress = startAddress || this.rom.header.nativeVectors.reset;
const finalAddress = endAddress || (currentAddress + 0x1000); // Default 4KB
// Check cache for this disassembly range first
const cacheParams = { start: currentAddress, end: finalAddress };
const cachedLines = this.cache.getDisassembly(this.rom, cacheParams);
if (cachedLines) {
this.logger.debug('Using cached disassembly', cacheParams);
return cachedLines;
}
const lines = [];
// Convert to ROM offset
let romOffset;
try {
romOffset = rom_parser_1.RomParser.getRomOffset(currentAddress, this.rom.cartridgeInfo);
}
catch (error) {
throw new Error(`Invalid start address: $${currentAddress.toString(16).toUpperCase()}`);
}
while (currentAddress < finalAddress && romOffset < this.rom.data.length) {
const line = this.decoder.decode(this.rom.data, romOffset, currentAddress);
if (!line) {
break;
}
// Add label if exists
if (this.labels.has(currentAddress)) {
line.label = this.labels.get(currentAddress);
}
// Add comment if exists
if (this.comments.has(currentAddress)) {
line.comment = this.comments.get(currentAddress);
}
// Enhance with reference data if enabled
if (this.enhanceComments) {
const enhancedLine = this.enhanceLineWithReferenceData(line);
lines.push(enhancedLine);
}
else {
lines.push(line);
}
// Move to next instruction
currentAddress += line.instruction.bytes;
romOffset += line.instruction.bytes;
}
// Validate the disassembly if enabled
if (this.enableValidation && lines.length > 0) {
this.validateDisassembly(lines);
}
// Cache the disassembly results for future use
if (lines.length > 0) {
this.cache.setDisassembly(this.rom, lines, cacheParams);
}
return lines;
}
disassembleFunction(startAddress, maxInstructions = 100) {
const lines = [];
let currentAddress = startAddress;
let instructionCount = 0;
try {
let romOffset = rom_parser_1.RomParser.getRomOffset(currentAddress, this.rom.cartridgeInfo);
while (instructionCount < maxInstructions && romOffset < this.rom.data.length) {
const line = this.decoder.decode(this.rom.data, romOffset, currentAddress);
if (!line) {
break;
}
lines.push(line);
// Stop at function return instructions
if (line.instruction.mnemonic === 'RTS' ||
line.instruction.mnemonic === 'RTL' ||
line.instruction.mnemonic === 'RTI') {
break;
}
currentAddress += line.instruction.bytes;
romOffset += line.instruction.bytes;
instructionCount++;
}
}
catch (error) {
throw new Error(`Error disassembling function at $${startAddress.toString(16).toUpperCase()}: ${error}`);
}
return lines;
}
formatOutput(lines) {
const output = [];
// Add ROM header info
output.push('; SNES ROM Disassembly');
output.push(`; Title: ${this.rom.header.title}`);
output.push(`; Map Mode: ${this.rom.cartridgeInfo.type}`);
output.push(`; ROM Size: ${(this.rom.cartridgeInfo.romSize / 1024).toFixed(0)} KB`);
output.push(`; Reset Vector: $${this.rom.header.nativeVectors.reset.toString(16).toUpperCase().padStart(4, '0')}`);
if (this.rom.cartridgeInfo.specialChip) {
output.push(`; Special Chip: ${this.rom.cartridgeInfo.specialChip}`);
}
output.push('');
for (const line of lines) {
let lineStr = '';
// Add label if present
if (line.label) {
output.push(`${line.label}:`);
}
// Format address
lineStr += `$${line.address.toString(16).toUpperCase().padStart(6, '0')}: `;
// Format bytes (hex dump)
const bytesStr = line.bytes.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
lineStr += bytesStr.padEnd(12, ' ');
// Format instruction
lineStr += line.instruction.mnemonic.padEnd(4, ' ');
// Format operand with symbol resolution
const operandStr = this.formatOperandWithSymbols(line);
if (operandStr) {
lineStr += operandStr;
}
// Add comment if present
if (line.comment) {
lineStr = lineStr.padEnd(40, ' ') + `; ${line.comment}`;
}
output.push(lineStr);
}
return output.join('\n');
}
// Enhanced formatting with symbol resolution and cross-references
formatOutputWithAnalysis(lines) {
const output = [];
const symbols = this.analysisEngine.getSymbols();
const crossRefs = this.analysisEngine.getCrossReferences();
const functions = this.analysisEngine.getFunctions();
// Add header with analysis summary
output.push('; SNES ROM Disassembly with Analysis');
output.push(`; Title: ${this.rom.header.title}`);
output.push(`; Cartridge: ${this.rom.cartridgeInfo.type}`);
output.push(`; Functions detected: ${functions.size}`);
output.push(`; Symbols generated: ${symbols.size}`);
output.push(`; Data structures: ${this.analysisEngine.getDataStructures().size}`);
output.push('');
for (const line of lines) {
// Add cross-references before the instruction
const refs = crossRefs.get(line.address);
if (refs && refs.length > 0) {
output.push('; Cross-references:');
for (const ref of refs) {
const refSymbol = symbols.get(ref.fromAddress);
const fromLabel = refSymbol ? refSymbol.name : `$${ref.fromAddress.toString(16).toUpperCase()}`;
output.push(`; ${ref.type}: ${fromLabel}`);
}
}
let lineStr = '';
// Add function header if this is a function start
const func = functions.get(line.address);
if (func) {
output.push('');
output.push(`; Function: ${symbols.get(line.address)?.name || `func_${line.address.toString(16).toUpperCase()}`}`);
if (func.callers.size > 0) {
output.push(`; Called by: ${Array.from(func.callers).map(addr => symbols.get(addr)?.name || `$${addr.toString(16).toUpperCase()}`).join(', ')}`);
}
if (func.confidence < 1.0) {
output.push(`; Confidence: ${(func.confidence * 100).toFixed(0)}%`);
}
}
// Add label if present
if (line.label) {
output.push(`${line.label}:`);
}
// Format address
lineStr += `$${line.address.toString(16).toUpperCase().padStart(6, '0')}: `;
// Format bytes
const bytesStr = line.bytes.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
lineStr += bytesStr.padEnd(12, ' ');
// Format instruction
lineStr += line.instruction.mnemonic.padEnd(4, ' ');
// Format operand with enhanced symbol resolution
const operandStr = this.formatOperandWithSymbols(line);
if (operandStr) {
lineStr += operandStr;
}
// Enhanced comments
const comments = [];
if (line.comment) {
comments.push(line.comment);
}
// Add hardware register info
if (line.operand !== undefined) {
const symbol = symbols.get(line.operand);
if (symbol && symbol.type === 'CONSTANT') {
comments.push(`${symbol.name}`);
}
}
if (comments.length > 0) {
lineStr = lineStr.padEnd(40, ' ') + `; ${comments.join(', ')}`;
}
output.push(lineStr);
}
return output.join('\n');
}
formatOperandWithSymbols(line) {
if (line.operand === undefined) {
return this.decoder.formatOperand(line);
}
// Check for symbol
const symbols = this.analysisEngine.getSymbols();
const symbol = symbols.get(line.operand);
if (symbol) {
return symbol.name;
}
// Fall back to decoder formatting
return this.decoder.formatOperand(line);
}
addLabel(address, label) {
this.labels.set(address, label);
}
addComment(address, comment) {
this.comments.set(address, comment);
}
/**
* Export analyzed SPC state to an SPC file
*/
exportSPC(outputPath) {
// Log SPC audio extraction
this.logger.info('🎵 Extracting SPC audio state from ROM...');
// Check cache for audio state first to avoid redundant extraction
let extractedState = this.cache.getAudioState(this.rom);
if (!extractedState) {
// Perform full disassembly and analysis only if not cached
const lines = this.disassemble();
this.analyze();
// Initialize SPC state extractor and extract audio state
extractedState = spc_state_extractor_1.SPCStateExtractor.extractAudioState(lines, this.rom.data, this.rom.cartridgeInfo);
// Cache the audio state for future use
this.cache.setAudioState(this.rom, extractedState);
}
else {
this.logger.debug('Using cached audio state');
}
// Create SPC export metadata using extracted metadata
const spcMetadata = {
songTitle: extractedState.metadata.gameTitle || 'Unknown Game',
gameTitle: extractedState.metadata.gameTitle || this.rom.header.title.trim() || 'Unknown Game',
artist: 'Unknown Artist',
dumperName: 'SNESDisassembler',
comments: `Extracted from ${this.rom.header.title}\nMapping Mode: ${this.rom.cartridgeInfo.type}\nROM Size: ${(this.rom.cartridgeInfo.romSize / 1024).toFixed(0)} KB`,
dumpDate: new Date().toLocaleDateString('en-US'),
playTime: extractedState.metadata.playTime || 180, // 3 minutes default
fadeLength: extractedState.metadata.fadeLength || 10000 // 10 seconds fade
};
// Log SPC metadata details
this.logger.info('📋 SPC Metadata:', spcMetadata);
// Export SPC file using static method
const spcBuffer = spc_exporter_1.SPCExporter.exportSPC(extractedState.spc700State, extractedState.dspState, spcMetadata);
// Log SPC export success
this.logger.info(`✅ SPC file exported`, {
bufferSize: spcBuffer.length,
ramSize: extractedState.spc700State.ram?.length || 0,
dspRegisterCount: extractedState.dspState.registers?.length || 0
});
// Write SPC file to disk
const fs = require('fs');
fs.writeFileSync(outputPath, spcBuffer);
// Log file write completion
this.logger.info(`💾 SPC exported to ${outputPath}`);
return spcBuffer;
}
// Enhanced analysis using the analysis engine
analyze() {
// Prevent recursion by returning early if already analyzing
if (this.isAnalyzing) {
this.logger.warn('Already analyzing, skipping to prevent recursion.');
return { functions: [], data: [] };
}
this.isAnalyzing = true;
try {
// Generate hash of current analysis context to detect changes
const analysisContext = {
romSize: this.rom.data.length,
cartridgeType: this.rom.cartridgeInfo.type,
labels: Array.from(this.labels.entries()),
comments: Array.from(this.comments.entries())
};
const contextHash = require('crypto').createHash('md5').update(JSON.stringify(analysisContext)).digest('hex');
// Skip redundant analysis if context hasn't changed
if (this.lastAnalysisHash === contextHash) {
this.logger.debug('Skipping redundant analysis - context unchanged');
const functions = Array.from(this.analysisEngine.getFunctions().keys());
const data = Array.from(this.analysisEngine.getDataStructures().keys());
return { functions, data };
}
// Check cache for function analysis results
const cachedFunctions = this.cache.getFunctions(this.rom, analysisContext);
if (cachedFunctions) {
this.logger.debug('Using cached function analysis');
this.lastAnalysisHash = contextHash;
// Restore cached symbols to labels
const symbols = this.analysisEngine.getSymbols();
for (const [address, symbol] of symbols) {
if (!this.labels.has(address)) {
this.labels.set(address, symbol.name);
}
}
return cachedFunctions;
}
// Perform full disassembly first (this checks its own cache)
const lines = this.disassemble();
// Extract vector addresses from ROM header (cache these too)
let vectorAddresses = this.cache.getVectors(this.rom);
if (!vectorAddresses) {
vectorAddresses = [
this.rom.header.nativeVectors.reset,
this.rom.header.nativeVectors.nmi,
this.rom.header.nativeVectors.irq,
this.rom.header.nativeVectors.cop,
this.rom.header.nativeVectors.brk,
this.rom.header.nativeVectors.abort,
this.rom.header.emulationVectors.reset,
this.rom.header.emulationVectors.nmi,
this.rom.header.emulationVectors.irq,
this.rom.header.emulationVectors.cop,
this.rom.header.emulationVectors.brk,
this.rom.header.emulationVectors.abort
].filter(addr => addr > 0); // Filter out invalid addresses
this.cache.setVectors(this.rom, vectorAddresses);
}
// Run comprehensive analysis
this.analysisEngine.analyze(lines, this.rom.cartridgeInfo, vectorAddresses);
// Extract results
const functions = Array.from(this.analysisEngine.getFunctions().keys());
const data = Array.from(this.analysisEngine.getDataStructures().keys());
const result = { functions, data };
// Update labels with generated symbols
const symbols = this.analysisEngine.getSymbols();
for (const [address, symbol] of symbols) {
if (!this.labels.has(address)) {
this.labels.set(address, symbol.name);
}
}
// Cache the analysis results
this.cache.setFunctions(this.rom, result, analysisContext);
this.lastAnalysisHash = contextHash;
return result;
}
finally {
// Always reset the analyzing flag
this.isAnalyzing = false;
}
}
// Get analysis results
getAnalysisResults() {
return {
controlFlowGraph: this.analysisEngine.getControlFlowGraph(),
symbols: this.analysisEngine.getSymbols(),
crossReferences: this.analysisEngine.getCrossReferences(),
dataStructures: this.analysisEngine.getDataStructures(),
functions: this.analysisEngine.getFunctions()
};
}
analyzeFunction(address, functions, data, visited = new Set()) {
if (visited.has(address) || functions.includes(address)) {
return;
}
visited.add(address);
functions.push(address);
try {
const lines = this.disassembleFunction(address);
for (const line of lines) {
// Look for branch/jump targets
if (line.instruction.mnemonic.startsWith('B') ||
line.instruction.mnemonic.startsWith('J')) {
if (line.operand !== undefined) {
// Recursively analyze branch/jump targets
this.analyzeFunction(line.operand, functions, data, visited);
}
}
}
}
catch (error) {
// If we can't disassemble, treat as data
data.push(address);
}
}
// ============================================================================
// Phase 4: Output & Integration - Multiple Output Formats
// ============================================================================
/**
* Generate output in specified format using Phase 4 formatters
*/
formatOutputAs(lines, format, options = {}) {
// Convert analysis symbols to output format
const symbols = this.convertAnalysisSymbolsToOutputFormat();
const crossRefs = this.generateCrossReferences(lines);
const formatter = output_formats_extended_1.ExtendedOutputFormatterFactory.create(format, this.rom, symbols, crossRefs, options);
return formatter.format(lines);
}
/**
* Export disassembly to file with automatic format detection
*/
exportToFile(filePath, format, options = {}) {
const lines = this.disassemble();
// Auto-detect format from file extension if not specified
if (!format) {
const ext = filePath.split('.').pop()?.toLowerCase();
switch (ext) {
case 's':
format = 'ca65';
break;
case 'asm':
format = 'wla-dx';
break;
case 'html':
format = 'html';
break;
case 'json':
format = 'json';
break;
case 'xml':
format = 'xml';
break;
case 'csv':
format = 'csv';
break;
case 'md':
format = 'markdown';
break;
default:
format = 'ca65';
break;
}
}
const output = this.formatOutputAs(lines, format, options);
// Write to file (Note: In a real implementation, you'd use fs.writeFileSync)
// For now, we'll return the output since we can't write files directly
this.logger.info(`Exporting to ${filePath} in ${format} format...`);
this.logger.debug(output.substring(0, 500) + '...[truncated]');
}
/**
* Get symbol manager for advanced symbol operations
*/
getSymbolManager() {
return this.symbolManager;
}
/**
* Import symbols from external file
*/
importSymbols(filePath, format) {
const result = this.symbolManager.importFromFile(filePath, format);
// Sync imported symbols with labels map
const allSymbols = this.symbolManager.getAllSymbols();
for (const [address, symbol] of allSymbols) {
if (!this.labels.has(address)) {
this.labels.set(address, symbol.name);
}
}
this.logger.info(`Imported ${result.succeeded} symbols, ${result.failed} failed`);
if (result.conflicts.length > 0) {
this.logger.warn(`${result.conflicts.length} conflicts detected during symbol import.`);
}
}
/**
* Export symbols to external file
*/
exportSymbols(filePath, format) {
// Sync current labels with symbol manager
this.syncLabelsToSymbolManager();
this.symbolManager.exportToFile(filePath, format || 'sym');
this.logger.info(`Exported ${this.symbolManager.getAllSymbols().size} symbols to ${filePath}`);
}
/**
* Generate comprehensive documentation in multiple formats
*/
generateDocumentation(outputDir) {
const lines = this.disassemble();
const analysis = this.getAnalysisResults();
const formats = [
{ format: 'html', filename: 'disassembly.html', options: { includeCrossReferences: true, includeSymbols: true } },
{ format: 'markdown', filename: 'README.md', options: { includeComments: true, includeSymbols: true } },
{ format: 'json', filename: 'disassembly.json', options: { includeSymbols: true, includeCrossReferences: true } },
{ format: 'ca65', filename: 'game.s', options: { includeHeader: true, includeComments: true } }
];
for (const { format, filename, options } of formats) {
const output = this.formatOutputAs(lines, format, options);
this.logger.info(`Generated ${filename} with ${output.length} characters.`);
// In a real implementation: fs.writeFileSync(path.join(outputDir, filename), output);
}
// Generate symbol table
this.exportSymbols(`${outputDir}/symbols.sym`);
this.logger.info(`Documentation generated in ${outputDir}/`);
this.logger.info('Files: disassembly.html, README.md, disassembly.json, game.s, symbols.sym');
}
/**
* Get supported output formats
*/
static getSupportedFormats() {
return output_formats_extended_1.ExtendedOutputFormatterFactory.getSupportedFormats();
}
/**
* Convert analysis engine symbols to output formatter format
*/
convertAnalysisSymbolsToOutputFormat() {
const outputSymbols = new Map();
const analysisSymbols = this.analysisEngine.getSymbols();
for (const [address, symbol] of analysisSymbols) {
outputSymbols.set(address, {
address,
name: symbol.name,
type: this.convertSymbolType(symbol.type),
scope: 'GLOBAL',
description: 'Auto-generated symbol'
});
}
// Add labels as symbols
for (const [address, label] of this.labels) {
if (!outputSymbols.has(address)) {
outputSymbols.set(address, {
address,
name: label,
type: 'CODE',
scope: 'GLOBAL'
});
}
}
return outputSymbols;
}
/**
* Convert analysis engine symbol types to output format
*/
convertSymbolType(type) {
switch (type.toUpperCase()) {
case 'FUNCTION': return 'CODE';
case 'DATA': return 'DATA';
case 'VECTOR': return 'VECTOR';
case 'REGISTER': return 'REGISTER';
case 'CONSTANT': return 'CONSTANT';
default: return 'CODE';
}
}
/**
* Generate cross-references from disassembly lines
*/
generateCrossReferences(lines) {
const crossRefs = [];
for (const line of lines) {
if (line.operand !== undefined) {
const refType = this.determineReferenceType(line.instruction.mnemonic);
if (refType) {
crossRefs.push({
fromAddress: line.address,
toAddress: line.operand,
type: refType,
instruction: line.instruction.mnemonic
});
}
}
}
return crossRefs;
}
/**
* Determine reference type from instruction mnemonic
*/
determineReferenceType(mnemonic) {
if (mnemonic === 'JSR' || mnemonic === 'JSL')
return 'CALL';
if (mnemonic === 'JMP' || mnemonic === 'JML')
return 'JUMP';
if (mnemonic.startsWith('B'))
return 'BRANCH';
if (mnemonic === 'LDA' || mnemonic === 'LDX' || mnemonic === 'LDY')
return 'DATA_READ';
if (mnemonic === 'STA' || mnemonic === 'STX' || mnemonic === 'STY')
return 'DATA_WRITE';
return null;
}
/**
* Sync current labels with symbol manager
*/
syncLabelsToSymbolManager() {
for (const [address, label] of this.labels) {
const existingSymbol = this.symbolManager.getSymbol(address);
if (!existingSymbol) {
this.symbolManager.addSymbol(address, {
address,
name: label,
type: 'CODE',
scope: 'GLOBAL'
});
}
}
}
// ============================================================================
// SNES Reference Integration Methods
// ============================================================================
/**
* Validate disassembly using SNES reference tables
*/
validateDisassembly(lines) {
// Check cache for validation results first
const validationParams = {
lineCount: lines.length,
addressRange: lines.length > 0 ? {
start: lines[0].address,
end: lines[lines.length - 1].address
} : null
};
const cachedResult = this.cache.getValidationResult(this.rom, validationParams);
if (cachedResult) {
this.logger.debug('Using cached validation result');
return cachedResult;
}
this.logger.info('🔍 Validating disassembly against SNES reference tables...');
const result = this.validationEngine.validateDisassembly(lines);
// Cache the validation result
this.cache.setValidationResult(this.rom, result, validationParams);
// Log detailed validation breakdown instead of just total counts
this.logValidationBreakdown(result);
this.logger.info(`✅ Validation complete with ${result.accuracy.toFixed(1)}% accuracy.`);
return result;
}
/**
* Enhance a disassembly line with reference data
*/
enhanceLineWithReferenceData(line) {
if (!line.instruction)
return line;
const { opcode, mnemonic } = line.instruction;
const { operand } = line;
// Generate instruction comment from reference data
const instructionComment = (0, snes_reference_tables_1.generateInstructionComment)(opcode, operand);
// Generate register comment if this is a register operation
let registerComment = '';
if (operand !== undefined && this.isRegisterAddress(operand)) {
registerComment = (0, snes_reference_tables_1.generateRegisterComment)(operand, this.getOperationType(mnemonic) || 'read');
}
// Combine comments
const comments = [line.comment, instructionComment, registerComment]
.filter(Boolean)
.join(' | ');
return {
...line,
comment: comments || line.comment
};
}
/**
* Check if an address is a SNES hardware register
*/
isRegisterAddress(address) {
return (address >= 0x2100 && address <= 0x21FF) || // PPU registers
(address >= 0x4200 && address <= 0x43FF) || // CPU registers
(address >= 0x2140 && address <= 0x2143); // APU I/O ports
}
/**
* Determine operation type from instruction mnemonic
*/
getOperationType(mnemonic) {
const writeInstructions = ['STA', 'STX', 'STY', 'STZ'];
const readInstructions = ['LDA', 'LDX', 'LDY', 'ADC', 'SBC', 'CMP', 'CPX', 'CPY'];
if (writeInstructions.includes(mnemonic))
return 'write';
if (readInstructions.includes(mnemonic))
return 'read';
return null;
}
/**
* Log detailed validation breakdown with examples and enhancements
*/
logValidationBreakdown(result) {
if (result.discrepancies.length === 0) {
this.logger.info('✅ No validation issues found.');
return;
}
// Group discrepancies by type
const discrepanciesByType = result.discrepancies.reduce((acc, discrepancy) => {
if (!acc[discrepancy.type]) {
acc[discrepancy.type] = [];
}
acc[discrepancy.type].push(discrepancy);
return acc;
}, {});
// Log summary counts by type
const typeCounts = Object.entries(discrepanciesByType).map(([type, discs]) => {
const errors = discs.filter(d => d.severity === 'error').length;
const warnings = discs.filter(d => d.severity === 'warning').length;
const infos = discs.filter(d => d.severity === 'info').length;
return `${type}: ${errors} errors, ${warnings} warnings, ${infos} info`;
});
this.logger.info(`⚠️ Validation issues breakdown (${result.discrepancies.length} total):`);
for (const typeCount of typeCounts) {
this.logger.info(` - ${typeCount}`);
}
// Log examples of first 3 errors of each type with debug level
for (const [type, discrepancies] of Object.entries(discrepanciesByType)) {
const examples = discrepancies.slice(0, 3);
this.logger.debug(`${type.toUpperCase()} examples:`);
for (const [index, example] of examples.entries()) {
const addressStr = `$${example.address.toString(16).toUpperCase().padStart(6, '0')}`;
this.logger.debug(` ${index + 1}. ${addressStr}: [${example.severity.toUpperCase()}] ${example.message}`);
// Add additional context if available
if (example.expected && example.actual) {
this.logger.debug(` Expected: ${JSON.stringify(example.expected)}`);
this.logger.debug(` Actual: ${JSON.stringify(example.actual)}`);
}
}
if (discrepancies.length > 3) {
this.logger.debug(` ... and ${discrepancies.length - 3} more ${type} issues`);
}
}
// Log summary of enhancements suggested
if (result.enhancements.length > 0) {
const enhancementsByType = result.enhancements.reduce((acc, enhancement) => {
if (!acc[enhancement.type]) {
acc[enhancement.type] = 0;
}
acc[enhancement.type]++;
return acc;
}, {});
const enhancementSummary = Object.entries(enhancementsByType)
.map(([type, count]) => `${count} ${type}`)
.join(', ');
this.logger.info(`💡 Enhancements available: ${enhancementSummary} (${result.enhancements.length} total)`);
// Log high priority enhancement examples
const highPriorityEnhancements = result.enhancements
.filter(e => e.priority === 'high')
.slice(0, 3);
if (highPriorityEnhancements.length > 0) {
this.logger.debug('High priority enhancement examples:');
for (const [index, enhancement] of highPriorityEnhancements.entries()) {
const addressStr = `$${enhancement.address.toString(16).toUpperCase().padStart(6, '0')}`;
this.logger.debug(` ${index + 1}. ${addressStr}: ${enhancement.content}`);
}
}
}
// Log recommended improvements if available
if (result.summary.recommendedImprovements.length > 0) {
this.logger.info('🔧 Recommended improvements:');
for (const improvement of result.summary.recommendedImprovements.slice(0, 3)) {
this.logger.info(` - ${improvement}`);
}
if (result.summary.recommendedImprovements.length > 3) {
this.logger.info(` ... and ${result.summary.recommendedImprovements.length - 3} more recommendations`);
}
}
}
/**
* Get validation results for the last disassembly
*/
getValidationResults() {
// Return the last validation result if validation is enabled
if (!this.enableValidation) {
this.logger.warn('Validation is disabled. Enable it in DisassemblerOptions to get validation results.');
return null;
}
// Use cached validation result if available, otherwise run validation
const lines = this.disassemble(); // This will use cache if available
const validationParams = {
lineCount: lines.length,
addressRange: lines.length > 0 ? {
start: lines[0].address,
end: lines[lines.length - 1].address
} : null
};
let result = this.cache.getValidationResult(this.rom, validationParams);
if (!result) {
result = this.validationEngine.validateDisassembly(lines);
this.cache.setValidationResult(this.rom, result, validationParams);
}
return result;
}
/**
* Generate a validation report for the current disassembly
*/
generateValidationReport() {
const result = this.getValidationResults();
if (!result) {
return 'Validation is disabled. No report available.';
}
return this.validationEngine.generateValidationReport(result);
}
/**
* Enable or disable reference-based validation
*/
setValidationEnabled(enabled) {
this.enableValidation = enabled;
}
/**
* Enable or disable comment enhancement
*/
setCommentEnhancementEnabled(enabled) {
this.enhanceComments = enabled;
}
/**
* Get reference data for a specific instruction opcode
*/
getInstructionReference(opcode) {
return snes_reference_tables_1.INSTRUCTION_REFERENCE[opcode];
}
/**
* Validate a specific instruction against reference data
*/
validateInstructionOpcode(opcode, expectedMnemonic, expectedBytes) {
return (0, snes_reference_tables_1.validateInstruction)(opcode, expectedMnemonic, expectedBytes);
}
/**
* Validate a register access
*/
validateRegisterAccess(address, operation) {
return (0, snes_reference_tables_1.validateRegister)(address, operation);
}
}
exports.SNESDisassembler = SNESDisassembler;
//# sourceMappingURL=disassembler.js.map