UNPKG

@five-vm/cli

Version:

High-performance CLI for Five VM development with WebAssembly integration

478 lines 19.2 kB
/** * Five CLI Compile Command * * Real compilation command using Five VM WASM bindings for DSL compilation, * ABI generation, and bytecode analysis. */ import { readFile, writeFile, stat, mkdir } from 'fs/promises'; import { join, dirname, extname, basename } from 'path'; import { glob } from 'glob'; import chalk from 'chalk'; import { FiveSDK } from '../sdk/index.js'; /** * Five compile command implementation */ export const compileCommand = { name: 'compile', description: 'Compile Five source to bytecode', aliases: ['c', 'build'], options: [ { flags: '-o, --output <file>', description: 'Output file path (default: <input>.bin)', required: false }, { flags: '-t, --target <target>', description: 'Compilation target', choices: ['vm', 'solana', 'debug', 'test'], defaultValue: 'vm' }, { flags: '-O, --optimize [level]', description: 'Enable optimizations (0-3, default: 2)', defaultValue: false }, { flags: '--optimization <level>', description: 'Optimization level for Five VM', choices: ['v1', 'v2', 'v3'], required: false }, { flags: '--debug', description: 'Include debug information in output', defaultValue: false }, { flags: '--abi <file>', description: 'Generate ABI file', required: false }, { flags: '--analyze', description: 'Perform bytecode analysis and show report', defaultValue: false }, { flags: '--watch', description: 'Watch for file changes and recompile', defaultValue: false }, { flags: '--validate', description: 'Validate syntax without compilation', defaultValue: false } ], arguments: [ { name: 'input', description: 'Input Five source file(s) or glob pattern', required: true, variadic: true } ], examples: [ { command: 'five compile src/main.v', description: 'Compile a single Five source file' }, { command: 'five compile src/**/*.v -o build/', description: 'Compile all Five files in src directory' }, { command: 'five compile src/main.v -t solana -O 3 --abi main.abi.json', description: 'Compile for Solana with maximum optimization and ABI generation' }, { command: 'five compile src/main.v --analyze --debug', description: 'Compile with debug info and bytecode analysis' }, { command: 'five compile src/**/*.v --watch', description: 'Watch for changes and auto-recompile' }, { command: 'five compile src/main.v --optimization v3', description: 'Compile with V3 pattern fusion optimizations' } ], handler: async (args, options, context) => { const { logger } = context; try { // Initialize SDK silently // Resolve input files const inputFiles = await resolveInputFiles(args, logger); if (inputFiles.length === 0) { throw new Error('No Five source files found'); } // Only show file count in verbose mode if (context.options.verbose) { logger.info(`Found ${inputFiles.length} source file(s) to compile`); } // Handle different modes if (options.validate) { await validateFiles(inputFiles, context); } else if (options.watch) { await watchAndCompile(inputFiles, options, context); } else { await compileFiles(inputFiles, options, context); } } catch (error) { logger.error('Compilation failed:', error); throw error; } } }; /** * Resolve input files from arguments and glob patterns */ async function resolveInputFiles(args, logger) { const inputFiles = []; for (const arg of args) { try { const stats = await stat(arg); if (stats.isFile() && extname(arg) === '.v') { inputFiles.push(arg); } else if (stats.isDirectory()) { // Search for .v files in directory const files = await glob(join(arg, '**/*.v')); inputFiles.push(...files); } } catch { // Try as glob pattern const files = await glob(arg); const fiveFiles = files.filter(file => extname(file) === '.v'); inputFiles.push(...fiveFiles); } } return [...new Set(inputFiles)]; // Remove duplicates } /** * Validate files without compilation using Five SDK */ async function validateFiles(inputFiles, context) { const { logger } = context; let allValid = true; for (const inputFile of inputFiles) { try { const sourceCode = await readFile(inputFile, 'utf8'); // Use Five SDK to validate bytecode const result = await FiveSDK.compile(sourceCode, { debug: false }); if (result.success) { console.log(chalk.green(`✓ ${inputFile} - Valid syntax`)); } else { console.log(chalk.red(`✗ ${inputFile} - Syntax errors:`)); result.errors?.forEach(error => { console.log(chalk.red(` • ${error.message}`)); }); allValid = false; } } catch (error) { console.log(chalk.red(`✗ ${inputFile} - Failed to read: ${error}`)); allValid = false; } } if (!allValid) { process.exit(1); } } /** * Compile multiple files */ async function compileFiles(inputFiles, options, context) { const { logger } = context; const results = []; for (const inputFile of inputFiles) { const startTime = Date.now(); try { if (context.options.verbose) { logger.info(`Compiling ${inputFile}...`); } const result = await compileSingleFile(inputFile, options, context); const duration = Date.now() - startTime; results.push({ file: inputFile, success: result.success, duration }); if (result.success) { console.log(chalk.green(`✓ ${inputFile}`)); console.log(chalk.gray(` ${result.metrics.sourceSize} chars → ${result.metrics.bytecodeSize} bytes (${duration}ms)`)); } else { console.log(chalk.red(`✗ ${inputFile} failed`)); // Display compilation errors (enhanced error system) if (result.errors) { for (const error of result.errors) { // Check if this is an enhanced error with rich information if (error.code && error.severity) { const enhancedError = error; // Display the error with code and category console.error(chalk.red(` Error ${enhancedError.code}: ${enhancedError.message}`)); // Display description if available if (enhancedError.description) { console.error(chalk.gray(` ${enhancedError.description}`)); } // Display source location if available if (enhancedError.location) { const loc = enhancedError.location; console.error(chalk.gray(` at line ${loc.line}, column ${loc.column}${loc.file ? ` in ${loc.file}` : ''}`)); } // Display suggestions if available if (enhancedError.suggestions && enhancedError.suggestions.length > 0) { console.error(chalk.cyan(` Suggestions:`)); for (const suggestion of enhancedError.suggestions) { console.error(chalk.cyan(` • ${suggestion.message}`)); if (suggestion.code_suggestion) { console.error(chalk.gray(` try: ${suggestion.code_suggestion}`)); } } } } else { // Basic error display fallback console.error(chalk.red(` Error: ${error.message}`)); if (error.sourceLocation) { console.error(chalk.gray(` at ${error.sourceLocation}`)); } } } } } } catch (error) { const duration = Date.now() - startTime; results.push({ file: inputFile, success: false, duration }); console.log(chalk.red(`✗ ${inputFile} failed`)); // Handle different error types if (error instanceof Error) { console.error(chalk.red(` Error: ${error.message}`)); if (error.stack) { logger.debug(`Error stack: ${error.stack}`); } } else { console.error(chalk.red(` Error: ${String(error)}`)); } } } // Summary const successful = results.filter(r => r.success).length; const failed = results.length - successful; const totalTime = results.reduce((sum, r) => sum + r.duration, 0); // Only show summary for multiple files or if verbose if (results.length > 1 || context.options.verbose) { console.log(`\n${chalk.green(`✓ ${successful}`)} ${failed > 0 ? chalk.red(`✗ ${failed}`) : ''}`); if (context.options.verbose) { console.log(chalk.gray(`Total: ${totalTime}ms`)); } } if (failed > 0) { process.exit(1); } } /** * Compile a single file using Five SDK */ async function compileSingleFile(inputFile, options, context) { const { logger } = context; // Read source code const sourceCode = await readFile(inputFile, 'utf8'); // Determine output file - default to .five format const outputFile = options.output || getDefaultOutputPath(inputFile, options.target, true); // Ensure output directory exists await mkdir(dirname(outputFile), { recursive: true }); // Prepare SDK compilation options const compilationOptions = { optimize: parseOptimizationLevel(options.optimize), debug: Boolean(options.debug), optimizationLevel: options.optimization || 'production' // Default to Production optimization for optimal header format }; // Compile using Five SDK (includes .five format generation) const result = await FiveSDK.compile(sourceCode, compilationOptions); // Write output file if successful if (result.success) { if (options.debug) { console.log('Debug: outputFile =', outputFile); console.log('Debug: result.fiveFile exists =', !!result.fiveFile); console.log('Debug: endsWith .five =', outputFile.endsWith('.five')); } if (outputFile.endsWith('.five') && result.fiveFile) { // Write .five format (default) await writeFile(outputFile, JSON.stringify(result.fiveFile, null, 2)); } else if (result.bytecode) { // Write raw bytecode (.bin format) await writeFile(outputFile, result.bytecode); } // Generate separate ABI if requested if (options.abi && result.metadata) { const abiFile = options.abi; await writeFile(abiFile, JSON.stringify(result.metadata, null, 2)); } } // Perform bytecode analysis if requested if (options.analyze && result.success && result.bytecode) { try { const validation = await FiveSDK.validateBytecode(result.bytecode, { debug: true }); displayBytecodeAnalysis({ validation }, logger); } catch (error) { logger.warn(`Failed to analyze bytecode: ${error}`); } } // Convert result format for CLI compatibility return { success: result.success, errors: result.errors, metrics: { sourceSize: sourceCode.length, bytecodeSize: result.bytecode?.length || 0 } }; } /** * Watch files for changes and recompile */ async function watchAndCompile(inputFiles, options, context) { const { logger } = context; // Import chokidar dynamically for file watching const chokidar = await import('chokidar'); logger.info('Watching for file changes...'); // Initial compilation await compileFiles(inputFiles, { ...options, watch: false }, context); // Watch for changes const watcher = chokidar.watch(inputFiles, { persistent: true, ignoreInitial: true }); watcher.on('change', async (filePath) => { logger.info(`File changed: ${filePath}`); try { await compileSingleFile(filePath, options, context); } catch (error) { logger.error(`Recompilation failed: ${error}`); } }); // Handle graceful shutdown process.on('SIGINT', () => { logger.info('Stopping file watcher...'); watcher.close(); process.exit(0); }); } /** * Get default output path based on input file and target */ function getDefaultOutputPath(inputFile, target, useFiveFormat = true) { const dir = dirname(inputFile); const baseName = basename(inputFile, '.v'); // Use .five format by default, .bin for legacy const extensions = { vm: useFiveFormat ? '.five' : '.bin', solana: useFiveFormat ? '.five' : '.so', debug: useFiveFormat ? '.five' : '.debug.bin', test: useFiveFormat ? '.five' : '.test.bin' }; const ext = extensions[target] || (useFiveFormat ? '.five' : '.bin'); return join(dir, baseName + ext); } /** * Parse optimization level from command line option */ function parseOptimizationLevel(optimize) { if (optimize === false || optimize === 'false' || optimize === '0') { return false; } return true; } /** * Display bytecode analysis using real analyzer results */ function displayBytecodeAnalysis(analysis, logger) { console.log('\n' + chalk.bold('Bytecode Analysis:')); if (analysis.summary) { console.log(` Total size: ${analysis.summary.total_size} bytes`); console.log(` Instructions: ${analysis.summary.total_instructions}`); console.log(` Compute units: ${analysis.summary.total_compute_units}`); console.log(` Functions: ${analysis.summary.has_function_calls ? 'Yes' : 'No'}`); console.log(` Jumps: ${analysis.summary.has_jumps ? 'Yes' : 'No'}`); } if (analysis.stack_analysis) { console.log(` Max stack depth: ${analysis.stack_analysis.max_stack_depth}`); console.log(` Stack consistency: ${analysis.stack_analysis.is_consistent ? 'Valid' : 'Invalid'}`); } if (analysis.control_flow && analysis.control_flow.basic_blocks) { console.log(` Basic blocks: ${analysis.control_flow.basic_blocks.length}`); } // Show top 5 most frequent instructions if (analysis.instructions && analysis.instructions.length > 0) { console.log('\n' + chalk.bold('Instruction Breakdown:')); const instructionCounts = new Map(); for (const inst of analysis.instructions) { const count = instructionCounts.get(inst.name) || 0; instructionCounts.set(inst.name, count + 1); } const sorted = Array.from(instructionCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5); for (const [name, count] of sorted) { console.log(` ${name}: ${count} times`); } } } /** * Display opcode usage analysis */ function displayOpcodeAnalysis(opcodeUsage, opcodeAnalysis, logger) { console.log('\n' + chalk.bold('Opcode Usage Analysis:')); if (opcodeUsage) { console.log(` Total opcodes generated: ${opcodeUsage.total_opcodes}`); console.log(` Unique opcodes used: ${opcodeUsage.unique_opcodes}`); if (opcodeUsage.top_opcodes && opcodeUsage.top_opcodes.length > 0) { console.log('\n' + chalk.bold(' Top Used Opcodes:')); opcodeUsage.top_opcodes.slice(0, 5).forEach(([opcode, count], index) => { console.log(` ${index + 1}. ${opcode}: ${count} times`); }); } if (opcodeUsage.category_distribution) { console.log('\n' + chalk.bold(' Opcode Categories:')); Object.entries(opcodeUsage.category_distribution).forEach(([category, count]) => { console.log(` ${category}: ${count} opcodes`); }); } } if (opcodeAnalysis && opcodeAnalysis.summary) { console.log('\n' + chalk.bold('Comprehensive Opcode Analysis:')); console.log(` Available opcodes in Five VM: ${opcodeAnalysis.summary.total_opcodes_available}`); console.log(` Opcodes used by this script: ${opcodeAnalysis.summary.opcodes_used}`); console.log(` Opcodes unused: ${opcodeAnalysis.summary.opcodes_unused}`); console.log(` Usage percentage: ${opcodeAnalysis.summary.usage_percentage.toFixed(1)}%`); if (opcodeAnalysis.used_opcodes && opcodeAnalysis.used_opcodes.length > 0) { console.log('\n' + chalk.bold(' Used Opcodes:')); opcodeAnalysis.used_opcodes.slice(0, 10).forEach((opcode) => { console.log(` • ${chalk.green(opcode.name)} (used ${opcode.usage_count} times)`); }); if (opcodeAnalysis.used_opcodes.length > 10) { console.log(` ${chalk.gray(`... and ${opcodeAnalysis.used_opcodes.length - 10} more`)}`); } } if (opcodeAnalysis.unused_opcodes && opcodeAnalysis.unused_opcodes.length > 0) { console.log('\n' + chalk.bold(' Sample Unused Opcodes:')); opcodeAnalysis.unused_opcodes.slice(0, 8).forEach((opcode) => { console.log(` • ${chalk.red(opcode.name)}`); }); if (opcodeAnalysis.unused_opcodes.length > 8) { console.log(` ${chalk.gray(`... and ${opcodeAnalysis.unused_opcodes.length - 8} more unused opcodes`)}`); } } } } //# sourceMappingURL=compile.js.map