UNPKG

snes-disassembler

Version:

A Super Nintendo (SNES) ROM disassembler for 65816 assembly

553 lines 20 kB
"use strict"; /** * Phase 4: Symbol Table Management System * * Comprehensive symbol management with external file support: * - Symbol export/import (.sym, .mlb files) * - Symbol name suggestion based on usage patterns * - Symbol conflict resolution * - Bulk symbol operations * - Symbol validation and verification */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SymbolManager = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); class SymbolManager { constructor() { this.symbols = new Map(); this.nameToAddress = new Map(); this.reservedNames = new Set([ // 65816 registers 'A', 'X', 'Y', 'S', 'P', 'PC', 'PBR', 'DBR', 'D', // SNES hardware registers (common ones) 'INIDISP', 'OBSEL', 'OAMADDL', 'OAMADDH', 'OAMDATA', 'BGMODE', 'MOSAIC', 'BG1SC', 'BG2SC', 'BG3SC', 'BG4SC', 'BG12NBA', 'BG34NBA', 'BG1HOFS', 'BG1VOFS', 'BG2HOFS', 'BG2VOFS', 'BG3HOFS', 'BG3VOFS', 'BG4HOFS', 'BG4VOFS', 'VMAIN', 'VMADDL', 'VMADDH', 'VMDATAL', 'VMDATAH', 'CGADD', 'CGDATA', 'W12SEL', 'W34SEL', 'WOBJSEL', 'WH0', 'WH1', 'WH2', 'WH3', 'WBGLOG', 'WOBJLOG', 'TM', 'TS', 'TMW', 'TSW', 'CGWSEL', 'CGADSUB', 'COLDATA', 'SETINI', 'NMITIMEN', 'WRIO', 'WRMPYA', 'WRMPYB', 'WRDIVL', 'WRDIVH', 'WRDIVB', 'HTIMEL', 'HTIMEH', 'VTIMEL', 'VTIMEH', 'MDMAEN', 'HDMAEN', 'MEMSEL' ]); } /** * Add a symbol to the table */ addSymbol(address, entry) { const validation = this.validateSymbol(entry); if (!validation.isValid) { return validation; } // Check for conflicts const existingByAddress = this.symbols.get(address); const existingByName = this.nameToAddress.get(entry.name); if (existingByAddress && existingByAddress.name !== entry.name) { validation.errors.push(`Address ${address.toString(16).toUpperCase()} already has symbol '${existingByAddress.name}'`); validation.isValid = false; } if (existingByName && existingByName !== address) { validation.errors.push(`Symbol name '${entry.name}' already exists at address ${existingByName.toString(16).toUpperCase()}`); validation.isValid = false; } if (validation.isValid) { this.symbols.set(address, entry); this.nameToAddress.set(entry.name, address); } return validation; } /** * Remove a symbol by address */ removeSymbol(address) { const symbol = this.symbols.get(address); if (symbol) { this.symbols.delete(address); this.nameToAddress.delete(symbol.name); return true; } return false; } /** * Get symbol by address */ getSymbol(address) { return this.symbols.get(address); } /** * Get address by symbol name */ getAddress(name) { return this.nameToAddress.get(name); } /** * Get all symbols */ getAllSymbols() { return new Map(this.symbols); } /** * Validate a symbol entry */ validateSymbol(entry) { const result = { isValid: true, errors: [], warnings: [] }; // Check name validity if (!entry.name || entry.name.trim().length === 0) { result.errors.push('Symbol name cannot be empty'); result.isValid = false; } // Check for reserved names if (this.reservedNames.has(entry.name.toUpperCase())) { result.warnings.push(`Symbol name '${entry.name}' is a reserved name`); } // Check name format (assembly identifier rules) const nameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/; if (!nameRegex.test(entry.name)) { result.errors.push(`Symbol name '${entry.name}' is not a valid assembly identifier`); result.isValid = false; } // Check address validity if (entry.address < 0 || entry.address > 0xFFFFFF) { result.errors.push(`Address ${entry.address.toString(16)} is out of valid range (0x000000-0xFFFFFF)`); result.isValid = false; } // Check size validity if (entry.size !== undefined && entry.size <= 0) { result.errors.push('Symbol size must be positive'); result.isValid = false; } return result; } /** * Generate symbol names based on address patterns and context */ generateSymbolName(address, type, context) { const prefix = this.getSymbolPrefix(type, address); const suffix = address.toString(16).toUpperCase().padStart(6, '0'); let baseName = `${prefix}_${suffix}`; // Add context if provided if (context) { baseName = `${context}_${baseName}`; } // Ensure uniqueness let counter = 1; let finalName = baseName; while (this.nameToAddress.has(finalName)) { finalName = `${baseName}_${counter}`; counter++; } return finalName; } getSymbolPrefix(type, address) { switch (type) { case 'CODE': return this.isVectorAddress(address) ? 'vector' : 'func'; case 'DATA': return 'data'; case 'VECTOR': return 'vector'; case 'REGISTER': return 'reg'; case 'CONSTANT': return 'const'; default: return 'sym'; } } isVectorAddress(address) { // Common SNES vector addresses const vectorAddresses = [ 0xFFE4, 0xFFE6, 0xFFE8, 0xFFEA, 0xFFEC, 0xFFEE, // Native mode vectors 0xFFF4, 0xFFF6, 0xFFF8, 0xFFFA, 0xFFFC, 0xFFFE // Emulation mode vectors ]; return vectorAddresses.includes(address); } /** * Bulk symbol operations */ bulkAddSymbols(symbols) { const result = { succeeded: 0, failed: 0, conflicts: [], errors: [] }; for (const [address, entry] of symbols) { const validation = this.validateSymbol(entry); if (validation.isValid) { const existingByAddress = this.symbols.get(address); const existingByName = this.nameToAddress.get(entry.name); let hasConflict = false; if (existingByAddress && existingByAddress.name !== entry.name) { result.conflicts.push({ address, existingSymbol: existingByAddress, conflictingSymbol: entry, conflictType: 'ADDRESS_DUPLICATE' }); hasConflict = true; } if (existingByName && existingByName !== address) { result.conflicts.push({ address, existingSymbol: this.symbols.get(existingByName), conflictingSymbol: entry, conflictType: 'NAME_DUPLICATE' }); hasConflict = true; } if (!hasConflict) { this.symbols.set(address, entry); this.nameToAddress.set(entry.name, address); result.succeeded++; } else { result.failed++; } } else { result.errors.push(`Invalid symbol at ${address.toString(16)}: ${validation.errors.join(', ')}`); result.failed++; } } return result; } /** * Export symbols to various formats */ exportToFile(filePath, format) { const ext = path.extname(filePath).toLowerCase(); const actualFormat = format || this.getFormatFromExtension(ext); switch (actualFormat) { case 'sym': this.exportToSymFile(filePath); break; case 'mlb': this.exportToMLBFile(filePath); break; case 'json': this.exportToJSONFile(filePath); break; case 'csv': this.exportToCSVFile(filePath); break; default: throw new Error(`Unsupported export format: ${actualFormat}`); } } getFormatFromExtension(ext) { switch (ext) { case '.sym': return 'sym'; case '.mlb': return 'mlb'; case '.json': return 'json'; case '.csv': return 'csv'; default: return 'sym'; } } /** * Export to .sym format (No$SNS debugger format) */ exportToSymFile(filePath) { const lines = []; // Sort symbols by address const sortedSymbols = Array.from(this.symbols.entries()).sort((a, b) => a[0] - b[0]); for (const [address, symbol] of sortedSymbols) { // Format: XXXXXX SymbolName lines.push(`${address.toString(16).toUpperCase().padStart(6, '0')} ${symbol.name}`); } fs.writeFileSync(filePath, lines.join('\n'), 'utf8'); } /** * Export to .mlb format (MAME debugger format) */ exportToMLBFile(filePath) { const lines = []; // Sort symbols by address const sortedSymbols = Array.from(this.symbols.entries()).sort((a, b) => a[0] - b[0]); for (const [address, symbol] of sortedSymbols) { // Format: SymbolName = $XXXXXX ; Type: Description let line = `${symbol.name} = $${address.toString(16).toUpperCase().padStart(6, '0')}`; if (symbol.type || symbol.description) { line += ' ;'; if (symbol.type) { line += ` Type: ${symbol.type}`; } if (symbol.description) { line += ` ${symbol.description}`; } } lines.push(line); } fs.writeFileSync(filePath, lines.join('\n'), 'utf8'); } /** * Export to JSON format */ exportToJSONFile(filePath) { const data = { metadata: { exportedAt: new Date().toISOString(), symbolCount: this.symbols.size, format: 'SNES Disassembler Symbol Table' }, symbols: Array.from(this.symbols.entries()).map(([address, symbol]) => ({ ...symbol, addressHex: address.toString(16).toUpperCase().padStart(6, '0') })) }; fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8'); } /** * Export to CSV format */ exportToCSVFile(filePath) { const lines = []; // CSV Header lines.push('Address,AddressHex,Name,Type,Scope,Size,Description'); // Sort symbols by address const sortedSymbols = Array.from(this.symbols.entries()).sort((a, b) => a[0] - b[0]); for (const [address, symbol] of sortedSymbols) { const row = [ address.toString(), `"${address.toString(16).toUpperCase().padStart(6, '0')}"`, `"${symbol.name}"`, `"${symbol.type}"`, `"${symbol.scope}"`, symbol.size?.toString() || '', `"${(symbol.description || '').replace(/"/g, '""')}"` ]; lines.push(row.join(',')); } fs.writeFileSync(filePath, lines.join('\n'), 'utf8'); } /** * Import symbols from file */ importFromFile(filePath, format) { if (!fs.existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } const ext = path.extname(filePath).toLowerCase(); const actualFormat = format || this.getFormatFromExtension(ext); switch (actualFormat) { case 'sym': return this.importFromSymFile(filePath); case 'mlb': return this.importFromMLBFile(filePath); case 'json': return this.importFromJSONFile(filePath); case 'csv': return this.importFromCSVFile(filePath); default: throw new Error(`Unsupported import format: ${actualFormat}`); } } /** * Import from .sym format */ importFromSymFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split('\n').filter(line => line.trim().length > 0); const symbols = new Map(); for (const line of lines) { const match = line.trim().match(/^([0-9A-Fa-f]{6})\s+(.+)$/); if (match) { const address = parseInt(match[1], 16); const name = match[2].trim(); symbols.set(address, { address, name, type: 'CODE', // Default type for .sym files scope: 'GLOBAL' }); } } return this.bulkAddSymbols(symbols); } /** * Import from .mlb format */ importFromMLBFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split('\n').filter(line => line.trim().length > 0); const symbols = new Map(); for (const line of lines) { // Parse: SymbolName = $XXXXXX ; Type: Description const match = line.trim().match(/^([^=]+)=\s*\$([0-9A-Fa-f]+)(?:\s*;\s*(.+))?$/); if (match) { const name = match[1].trim(); const address = parseInt(match[2], 16); const comment = match[3]?.trim(); let type = 'CODE'; const description = comment; // Parse type from comment if (comment) { const typeMatch = comment.match(/Type:\s*(\w+)/i); if (typeMatch) { const typeStr = typeMatch[1].toUpperCase(); if (['CODE', 'DATA', 'VECTOR', 'REGISTER', 'CONSTANT'].includes(typeStr)) { type = typeStr; } } } symbols.set(address, { address, name, type, scope: 'GLOBAL', description }); } } return this.bulkAddSymbols(symbols); } /** * Import from JSON format */ importFromJSONFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); const data = JSON.parse(content); const symbols = new Map(); if (data.symbols && Array.isArray(data.symbols)) { for (const symbolData of data.symbols) { const address = typeof symbolData.address === 'number' ? symbolData.address : parseInt(symbolData.addressHex || symbolData.address, 16); symbols.set(address, { address, name: symbolData.name, type: symbolData.type || 'CODE', scope: symbolData.scope || 'GLOBAL', size: symbolData.size, description: symbolData.description, references: symbolData.references }); } } return this.bulkAddSymbols(symbols); } /** * Import from CSV format */ importFromCSVFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split('\n').filter(line => line.trim().length > 0); if (lines.length === 0) { throw new Error('CSV file is empty'); } // Skip header line const dataLines = lines.slice(1); const symbols = new Map(); for (const line of dataLines) { const columns = this.parseCSVLine(line); if (columns.length >= 4) { const address = parseInt(columns[0]); const name = columns[2].replace(/"/g, ''); const type = columns[3].replace(/"/g, '') || 'CODE'; const scope = columns[4]?.replace(/"/g, '') || 'GLOBAL'; const size = columns[5] ? parseInt(columns[5]) : undefined; const description = columns[6]?.replace(/"/g, '').replace(/""/g, '"'); symbols.set(address, { address, name, type, scope, size, description }); } } return this.bulkAddSymbols(symbols); } parseCSVLine(line) { const result = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { if (inQuotes && line[i + 1] === '"') { current += '"'; i++; // Skip next quote } else { inQuotes = !inQuotes; } } else if (char === ',' && !inQuotes) { result.push(current); current = ''; } else { current += char; } } result.push(current); return result; } /** * Clear all symbols */ clear() { this.symbols.clear(); this.nameToAddress.clear(); } /** * Get symbol statistics */ getStatistics() { const stats = { total: this.symbols.size, byType: {}, byScope: {} }; for (const symbol of this.symbols.values()) { stats.byType[symbol.type] = (stats.byType[symbol.type] || 0) + 1; stats.byScope[symbol.scope] = (stats.byScope[symbol.scope] || 0) + 1; } return stats; } } exports.SymbolManager = SymbolManager; //# sourceMappingURL=symbol-manager.js.map