@five-vm/cli
Version:
High-performance CLI for Five VM development with WebAssembly integration
478 lines • 19.2 kB
JavaScript
/**
* 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