snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
588 lines • 26.7 kB
JavaScript
"use strict";
/**
* SNES Disassembler Validation Engine
*
* Provides real-time validation and enhancement of disassembly output
* using authoritative SNES reference data from snes-mcp-server
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SNESValidationEngine = void 0;
exports.quickValidateOpcode = quickValidateOpcode;
exports.quickValidateRegister = quickValidateRegister;
exports.getInstructionReference = getInstructionReference;
exports.getRegisterReference = getRegisterReference;
exports.validateSNESDisassembly = validateSNESDisassembly;
const snes_reference_tables_1 = require("./snes-reference-tables");
const logger_1 = require("./utils/logger");
// =====================================================================
// VALIDATION ENGINE
// =====================================================================
class SNESValidationEngine {
constructor(logLevel = 'normal') {
this.instructionStats = new Map();
this.registerStats = new Map();
this.validationResults = [];
this.enhancements = [];
this.validationCache = new Map();
this.errorFrequency = new Map();
this.logger = (0, logger_1.createLogger)('SNESValidationEngine');
this.logLevel = logLevel;
}
/**
* Generate cache key for validation request
*/
generateValidationCacheKey(lines) {
// Create a hash based on instruction content for caching
const content = lines.map(line => `${line.address}:${line.instruction.opcode}:${line.operand || ''}`).join('|');
return require('crypto').createHash('md5').update(content).digest('hex').substring(0, 16);
}
/**
* Generate error key based on type and specific details for frequency tracking
*/
generateErrorKey(discrepancy) {
const { type, severity, message } = discrepancy;
// Create a normalized key for similar errors
let normalizedMessage = message;
// Normalize common patterns to group similar errors
if (type === 'instruction') {
// Group instruction errors by the general error type rather than specific opcodes
normalizedMessage = message
.replace(/\$[0-9A-Fa-f]+/g, '$XX') // Replace hex addresses with placeholder
.replace(/\b\d+\b/g, 'N'); // Replace numbers with placeholder
}
else if (type === 'register') {
// Group register errors by register type and operation
normalizedMessage = message
.replace(/\$[0-9A-Fa-f]+/g, '$XXXX') // Replace hex addresses with placeholder
.replace(/\bat address \$[0-9A-Fa-f]+/g, 'at address $XXXX');
}
else if (type === 'addressing') {
// Group addressing mode errors by the specific mode mismatch pattern
normalizedMessage = message; // Keep addressing mode errors as-is for now
}
return `${type}:${severity}:${normalizedMessage}`;
}
/**
* Validate a complete disassembly against SNES reference data
*/
validateDisassembly(lines) {
// Check cache first for this exact validation request
const cacheKey = this.generateValidationCacheKey(lines);
const cachedResult = this.validationCache.get(cacheKey);
if (cachedResult) {
if (this.logLevel === 'verbose') {
this.logger.debug('Using cached validation result', {
lines: lines.length,
cacheKey: cacheKey.substring(0, 8) + '...'
});
}
return cachedResult;
}
this.reset();
if (this.logLevel !== 'minimal') {
this.logger.info('🔍 Starting SNES reference validation...', { lines: lines.length });
}
// Validate each instruction
for (const line of lines) {
this.validateDisassemblyLine(line);
}
// Generate summary
const summary = this.generateSummary(lines);
// Log based on verbosity level
if (this.logLevel === 'verbose') {
this.logValidationSummary();
}
else if (this.logLevel === 'normal') {
this.logNormalSummary();
}
// For minimal, only log final summary
if (this.logLevel !== 'minimal') {
this.logger.info(`✅ Validation complete: ${summary.accuracyScore.toFixed(1)}% accuracy`);
}
else {
// Minimal: Only log final summary with error counts
this.logMinimalSummary(summary);
}
const result = {
isValid: summary.accuracyScore >= 90.0,
accuracy: summary.accuracyScore,
discrepancies: this.validationResults,
enhancements: this.enhancements,
summary
};
// Cache the result for future use
this.validationCache.set(cacheKey, result);
// Limit cache size to prevent memory issues
if (this.validationCache.size > 50) {
const firstKey = this.validationCache.keys().next().value;
if (firstKey) {
this.validationCache.delete(firstKey);
}
}
return result;
}
/**
* Validate a single disassembly line
*/
validateDisassemblyLine(line) {
if (!line.instruction)
return;
const { opcode, mnemonic, addressingMode } = line.instruction;
const { operand } = line;
// Track instruction usage
this.instructionStats.set(opcode, (this.instructionStats.get(opcode) || 0) + 1);
// Validate instruction against reference
const validation = (0, snes_reference_tables_1.validateInstruction)(opcode, mnemonic, line.bytes?.length);
if (!validation.isValid) {
// Add debug logging for instruction validation failures
if (this.logLevel === 'verbose') {
this.logger.debug('Instruction validation failed', {
address: `$${line.address.toString(16).toUpperCase().padStart(6, '0')}`,
opcode: `$${opcode.toString(16).toUpperCase().padStart(2, '0')}`,
actualMnemonic: mnemonic,
expectedMnemonic: validation.reference?.mnemonic || 'UNKNOWN',
actualByteLength: line.bytes?.length || 0,
expectedByteLength: validation.reference?.bytes || 0,
discrepancies: validation.discrepancies
});
}
// Log individual failures only in verbose mode
if (this.logLevel === 'verbose') {
this.logger.warn(`Invalid instruction at $${line.address.toString(16).toUpperCase().padStart(6, '0')}: ${validation.discrepancies.join(', ')}`);
}
const discrepancy = {
type: 'instruction',
severity: 'error',
message: validation.discrepancies.join(', '),
address: line.address,
actual: { opcode, mnemonic, bytes: line.bytes?.length }
};
this.validationResults.push(discrepancy);
// Track error frequency
const errorKey = this.generateErrorKey(discrepancy);
this.errorFrequency.set(errorKey, (this.errorFrequency.get(errorKey) || 0) + 1);
return;
}
const reference = validation.reference;
// Check addressing mode consistency
if (addressingMode && reference.addressingMode !== addressingMode) {
// Add debug logging for addressing mode mismatches
this.logger.debug('Addressing mode mismatch detected', {
address: `$${line.address.toString(16).toUpperCase().padStart(6, '0')}`,
opcode: `$${opcode.toString(16).toUpperCase().padStart(2, '0')}`,
mnemonic: mnemonic,
expectedMode: reference.addressingMode,
actualMode: addressingMode
});
this.logger.warn(`Addressing mode mismatch at $${line.address.toString(16).toUpperCase().padStart(6, '0')}: expected ${reference.addressingMode}, got ${addressingMode}`);
const discrepancy = {
type: 'addressing',
severity: 'warning',
message: `Addressing mode mismatch: expected ${reference.addressingMode}, got ${addressingMode}`,
address: line.address,
expected: reference.addressingMode,
actual: addressingMode,
reference
};
this.validationResults.push(discrepancy);
// Track error frequency
const errorKey = this.generateErrorKey(discrepancy);
this.errorFrequency.set(errorKey, (this.errorFrequency.get(errorKey) || 0) + 1);
}
// Generate enhanced instruction comment
const instructionComment = (0, snes_reference_tables_1.generateInstructionComment)(opcode, operand);
if (instructionComment) {
this.enhancements.push({
type: 'comment',
address: line.address,
content: instructionComment,
priority: 'medium'
});
}
// Validate register access if this is a register operation
if (operand !== undefined && this.isRegisterAddress(operand)) {
this.validateRegisterAccess(line.address, operand, mnemonic);
}
}
/**
* Validate register access against SNES hardware specifications
*/
validateRegisterAccess(address, registerAddr, mnemonic) {
const operation = this.getOperationType(mnemonic);
if (!operation)
return;
// Track register usage
const stats = this.registerStats.get(registerAddr) || { reads: 0, writes: 0 };
if (operation === 'read') {
stats.reads++;
}
else {
stats.writes++;
}
this.registerStats.set(registerAddr, stats);
// Validate against reference
const validation = (0, snes_reference_tables_1.validateRegister)(registerAddr, operation);
if (!validation.isValid) {
// Add debug logging for register validation failures
this.logger.debug('Register validation failed', {
address: `$${address.toString(16).toUpperCase().padStart(6, '0')}`,
registerAddress: `$${registerAddr.toString(16).toUpperCase().padStart(4, '0')}`,
operationType: operation,
mnemonic: mnemonic,
violations: validation.warnings,
reference: validation.reference
});
this.logger.warn(`Invalid register access at $${address.toString(16).toUpperCase().padStart(6, '0')}: ${operation} operation on register $${registerAddr.toString(16).toUpperCase().padStart(4, '0')} - ${validation.warnings.join(', ')}`);
const discrepancy = {
type: 'register',
severity: 'error',
message: validation.warnings.join(', '),
address,
actual: { register: registerAddr, operation }
};
this.validationResults.push(discrepancy);
// Track error frequency
const errorKey = this.generateErrorKey(discrepancy);
this.errorFrequency.set(errorKey, (this.errorFrequency.get(errorKey) || 0) + 1);
return;
}
// Add warnings for access violations
if (validation.warnings.length > 0) {
// Add debug logging for register access warnings
this.logger.debug('Register access warning', {
address: `$${address.toString(16).toUpperCase().padStart(6, '0')}`,
registerAddress: `$${registerAddr.toString(16).toUpperCase().padStart(4, '0')}`,
operationType: operation,
mnemonic: mnemonic,
warnings: validation.warnings,
reference: validation.reference
});
this.logger.warn(`Register access warning at $${address.toString(16).toUpperCase().padStart(6, '0')}: ${operation} operation on register $${registerAddr.toString(16).toUpperCase().padStart(4, '0')} - ${validation.warnings.join(', ')}`);
const discrepancy = {
type: 'register',
severity: 'warning',
message: validation.warnings.join(', '),
address,
reference: validation.reference
};
this.validationResults.push(discrepancy);
// Track error frequency
const errorKey = this.generateErrorKey(discrepancy);
this.errorFrequency.set(errorKey, (this.errorFrequency.get(errorKey) || 0) + 1);
}
// Generate enhanced register comment
const registerComment = (0, snes_reference_tables_1.generateRegisterComment)(registerAddr, operation);
if (registerComment) {
this.enhancements.push({
type: 'comment',
address,
content: registerComment,
priority: 'high'
});
}
// Add contextual information for important registers
const registerInfo = (0, snes_reference_tables_1.getRegisterInfo)(registerAddr);
if (registerInfo.name && this.isImportantRegister(registerAddr)) {
this.enhancements.push({
type: 'context',
address,
content: `${registerInfo.name}: ${registerInfo.description}`,
priority: 'high'
});
}
}
/**
* Generate comprehensive validation summary
*/
generateSummary(lines) {
const totalInstructions = lines.filter(line => line.instruction).length;
const instructionErrors = this.validationResults.filter(r => r.type === 'instruction').length;
const registerErrors = this.validationResults.filter(r => r.type === 'register').length;
const addressingErrors = this.validationResults.filter(r => r.type === 'addressing').length;
const validatedInstructions = totalInstructions - instructionErrors;
const totalRegisters = this.registerStats.size;
const validatedRegisters = totalRegisters - registerErrors;
const accuracyScore = totalInstructions > 0 ? (validatedInstructions / totalInstructions) * 100 : 0;
const recommendedImprovements = this.generateRecommendations();
return {
totalInstructions,
validatedInstructions,
totalRegisters,
validatedRegisters,
accuracyScore,
recommendedImprovements
};
}
/**
* Log detailed validation summary (verbose mode)
*/
logValidationSummary() {
const errorCategories = {
instruction: this.validationResults.filter(r => r.type === 'instruction').length,
register: this.validationResults.filter(r => r.type === 'register').length,
addressing: this.validationResults.filter(r => r.type === 'addressing').length
};
this.logger.info('Validation Error Summary:', errorCategories);
// Use the new getMostCommonErrors method for better insights
const topCommonErrors = this.getMostCommonErrors(5);
if (topCommonErrors.length > 0) {
this.logger.info('Top 5 Common Error Patterns:', {
patterns: topCommonErrors.map(([key, count]) => {
// Extract readable information from the error key
const [type, severity, message] = key.split(':', 3);
return {
type,
severity,
pattern: message,
count
};
})
});
}
const addressIssues = this.validationResults
.map(r => r.address)
.reduce((acc, address) => {
acc[address] = (acc[address] || 0) + 1;
return acc;
}, {});
const topAddressIssues = Object.entries(addressIssues)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
this.logger.info('Top Addresses with Issues:', { addresses: topAddressIssues });
// Log all individual validation failures in verbose mode
if (this.validationResults.length > 0) {
this.logger.info('Individual Validation Failures:');
for (const result of this.validationResults) {
const addressStr = `$${result.address.toString(16).toUpperCase().padStart(6, '0')}`;
this.logger.info(` ${addressStr}: [${result.severity.toUpperCase()}] ${result.type} - ${result.message}`);
}
}
}
/**
* Log normal validation summary (normal mode)
*/
logNormalSummary() {
const errorCategories = {
instruction: this.validationResults.filter(r => r.type === 'instruction').length,
register: this.validationResults.filter(r => r.type === 'register').length,
addressing: this.validationResults.filter(r => r.type === 'addressing').length
};
this.logger.info('Validation Error Summary:', errorCategories);
if (this.validationResults.length > 0) {
const commonErrors = this.validationResults
.map(r => r.message)
.reduce((acc, message) => {
acc[message] = (acc[message] || 0) + 1;
return acc;
}, {});
const topCommonErrors = Object.entries(commonErrors)
.sort((a, b) => b[1] - a[1])
.slice(0, 3); // Show fewer errors than verbose mode
this.logger.info('Top 3 Common Errors:', { errors: topCommonErrors });
}
}
/**
* Log minimal validation summary (minimal mode)
*/
logMinimalSummary(summary) {
const totalErrors = this.validationResults.filter(r => r.severity === 'error').length;
const totalWarnings = this.validationResults.filter(r => r.severity === 'warning').length;
this.logger.info(`Validation: ${summary.accuracyScore.toFixed(1)}% accuracy, ${totalErrors} errors, ${totalWarnings} warnings`);
}
/**
* Generate specific recommendations for improvement
*/
generateRecommendations() {
const recommendations = [];
// Check for common issues
const instructionErrors = this.validationResults.filter(r => r.type === 'instruction' && r.severity === 'error');
if (instructionErrors.length > 0) {
recommendations.push(`Fix ${instructionErrors.length} instruction decoding errors`);
}
const registerWarnings = this.validationResults.filter(r => r.type === 'register' && r.severity === 'warning');
if (registerWarnings.length > 0) {
recommendations.push(`Review ${registerWarnings.length} register access violations`);
}
// Check for missing common instructions
const commonOpcodes = [0x78, 0x9C, 0x20, 0xA9, 0x8D]; // SEI, STZ, JSR, LDA, STA
const missingCommon = commonOpcodes.filter(opcode => !this.instructionStats.has(opcode));
if (missingCommon.length > 0) {
recommendations.push('Add support for common missing opcodes: ' +
missingCommon.map(op => `$${op.toString(16).toUpperCase()}`).join(', '));
}
// Check for register coverage
const importantRegisters = [0x2100, 0x4200, 0x2140]; // INIDISP, NMITIMEN, APUIO0
const missingRegisters = importantRegisters.filter(addr => !this.registerStats.has(addr));
if (missingRegisters.length > 0) {
recommendations.push('Improve detection of important registers: ' +
missingRegisters.map(addr => `$${addr.toString(16).toUpperCase()}`).join(', '));
}
return recommendations;
}
/**
* Enhance disassembly output with reference data
*/
enhanceDisassemblyOutput(lines) {
const validationResult = this.validateDisassembly(lines);
return lines.map(line => {
if (!line.instruction)
return line;
// Find relevant enhancements for this line
const lineEnhancements = validationResult.enhancements.filter(e => e.address === line.address);
if (lineEnhancements.length === 0)
return line;
// Add enhanced comments
const comments = lineEnhancements
.filter(e => e.type === 'comment')
.map(e => e.content);
const context = lineEnhancements
.filter(e => e.type === 'context')
.map(e => e.content);
return {
...line,
comment: comments.length > 0 ? comments.join(' | ') : line.comment
};
});
}
/**
* Generate detailed validation report
*/
generateValidationReport(result) {
let report = '# SNES Disassembly Validation Report\\n\\n';
report += '## Summary\\n';
report += `- **Overall Accuracy**: ${result.accuracy.toFixed(1)}%\\n`;
report += `- **Instructions Validated**: ${result.summary.validatedInstructions}/${result.summary.totalInstructions}\\n`;
report += `- **Registers Validated**: ${result.summary.validatedRegisters}/${result.summary.totalRegisters}\\n`;
report += `- **Status**: ${result.isValid ? '✅ PASS' : '❌ FAIL'}\\n\\n`;
if (result.discrepancies.length > 0) {
report += '## Issues Found\\n\\n';
const errors = result.discrepancies.filter(d => d.severity === 'error');
const warnings = result.discrepancies.filter(d => d.severity === 'warning');
if (errors.length > 0) {
report += `### Errors (${errors.length})\\n`;
errors.forEach(error => {
report += `- **$${error.address.toString(16).toUpperCase()}**: ${error.message}\\n`;
});
report += '\\n';
}
if (warnings.length > 0) {
report += `### Warnings (${warnings.length})\\n`;
warnings.forEach(warning => {
report += `- **$${warning.address.toString(16).toUpperCase()}**: ${warning.message}\\n`;
});
report += '\\n';
}
}
if (result.summary.recommendedImprovements.length > 0) {
report += '## Recommended Improvements\\n\\n';
result.summary.recommendedImprovements.forEach(improvement => {
report += `- ${improvement}\\n`;
});
report += '\\n';
}
report += '## Reference Statistics\\n\\n';
report += `- **Instruction Coverage**: ${this.instructionStats.size} unique opcodes\\n`;
report += `- **Register Coverage**: ${this.registerStats.size} unique registers\\n`;
report += `- **Validation Rules Applied**: ${Object.keys(snes_reference_tables_1.INSTRUCTION_REFERENCE).length} instruction rules, ${Object.keys(snes_reference_tables_1.REGISTER_REFERENCE).length} register rules\\n`;
return report;
}
// =====================================================================
// UTILITY METHODS
// =====================================================================
reset() {
this.instructionStats.clear();
this.registerStats.clear();
this.validationResults = [];
this.enhancements = [];
this.errorFrequency.clear();
}
/**
* Clear validation cache
*/
clearCache() {
this.validationCache.clear();
this.logger.debug('Validation cache cleared');
}
/**
* Get cache statistics
*/
getCacheStats() {
return {
size: this.validationCache.size,
maxSize: 50
};
}
/**
* Get most common errors
*/
getMostCommonErrors(limit) {
return Array.from(this.errorFrequency.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, limit);
}
isRegisterAddress(address) {
// SNES hardware registers are in specific ranges
return (address >= 0x2100 && address <= 0x21FF) || // PPU registers
(address >= 0x4200 && address <= 0x43FF) || // CPU registers
(address >= 0x2140 && address <= 0x2143); // APU I/O ports
}
getOperationType(mnemonic) {
const writeInstructions = ['STA', 'STX', 'STY', 'STZ'];
const readInstructions = ['LDA', 'LDX', 'LDY', 'ADC', 'SBC', 'CMP', 'CPX', 'CPY', 'AND', 'ORA', 'EOR', 'BIT', 'TSB', 'TRB'];
const readModifyWriteInstructions = ['INC', 'DEC', 'ASL', 'LSR', 'ROL', 'ROR'];
if (writeInstructions.includes(mnemonic))
return 'write';
if (readInstructions.includes(mnemonic))
return 'read';
if (readModifyWriteInstructions.includes(mnemonic))
return 'write'; // These do both, count as write
return null;
}
isImportantRegister(address) {
const importantRegisters = [
0x2100, // INIDISP
0x4200, // NMITIMEN
0x2140, 0x2141, 0x2142, 0x2143, // APU I/O
0x420B, 0x420C, // DMA enable
0x2105, // BGMODE
0x212C, 0x212D // Screen enable
];
return importantRegisters.includes(address);
}
}
exports.SNESValidationEngine = SNESValidationEngine;
// =====================================================================
// EXPORT VALIDATION UTILITIES
// =====================================================================
/**
* Quick validation of an instruction opcode
*/
function quickValidateOpcode(opcode) {
return opcode in snes_reference_tables_1.INSTRUCTION_REFERENCE;
}
/**
* Quick validation of a register address
*/
function quickValidateRegister(address) {
return address in snes_reference_tables_1.REGISTER_REFERENCE;
}
/**
* Get reference data for an instruction
*/
function getInstructionReference(opcode) {
return snes_reference_tables_1.INSTRUCTION_REFERENCE[opcode];
}
/**
* Get reference data for a register
*/
function getRegisterReference(address) {
return snes_reference_tables_1.REGISTER_REFERENCE[address];
}
/**
* Standalone validation function for integration testing
*/
function validateSNESDisassembly(lines) {
const engine = new SNESValidationEngine();
return engine.validateDisassembly(lines);
}
//# sourceMappingURL=validation-engine.js.map