run method

  1. @override
Future<void> run(
  1. Iterable<String> args
)

Parses args and invokes Command.run on the chosen command.

This always returns a Future in case the command is asynchronous. The Future will throw a UsageException if args was invalid.

Implementation

@override
Future<void> run(Iterable<String> args) async {
  // artisanal.run(args) handles global setup and catches UsageException.
  // However, it expects subcommands. Since Lua uses flags for logic,
  // we need to handle the parsed results if no command matches.

  // Setup renderer etc by calling artisanal's run logic via super.run
  // But super.run will likely throw if we have no commands.

  // Let's use artisanal's public properties to mimic its behavior.

  final argResults = argParser.parse(args);

  if (argResults['help'] as bool) {
    printUsage();
    return;
  }

  if (argResults['version'] as bool) {
    final versionCmd = VersionCommand();
    await versionCmd.run();
    if (args.length == 1) return;
  }

  final restArgs = argResults.rest;
  final config = LuaLikeConfig();
  final useIr = argResults['ir'] as bool;
  final useLuaBytecode = argResults['lua-bytecode'] as bool;
  final useAst = argResults['ast'] as bool;
  // Precompiled chunks (detected by official Lua header bytes, not extension)
  // must use the bytecode VM so ScriptCommand can load+run without IR/SSA.
  final autoUseLuaBytecode =
      restArgs.isNotEmpty &&
      _looksLikeTrackedLuaBytecodeScript(restArgs.first);
  if (useLuaBytecode || autoUseLuaBytecode) {
    config.defaultEngineMode = EngineMode.luaBytecode;
  } else if (useIr || !useAst) {
    config.defaultEngineMode = EngineMode.ir;
  } else {
    config.defaultEngineMode = EngineMode.ast;
  }
  config.dumpIr = argResults['dump-ir'] as bool;
  config.emitLlvm = argResults['emit-llvm'] as bool;
  config.emitDart = argResults['emit-dart'] as bool;
  config.foldEnabled = argResults['fold'] as bool;
  // Handle --disassemble (print bytecode listing, no execution)
  if (argResults['disassemble'] as bool) {
    if (restArgs.isEmpty) {
      stderr.writeln('Error: --disassemble requires a script file argument.');
      exit(1);
    }
    final scriptPath = restArgs.first;
    final bytes = File(scriptPath).readAsBytesSync();
    try {
      // Official header → parse as binary chunk
      if (bytes.isNotEmpty && bytes.first == 0x1B) {
        final chunk = const LuaBytecodeParser().parse(bytes);
        print(const LuaBytecodeDisassembler().render(chunk));
      } else {
        final source = _decodeSource(bytes);
        final program = parse(source, url: scriptPath);
        final rawBc = argResults['raw'] as bool;
        final artifact =
            CompilePipeline(
                  config: CompilePipelineConfig.luaBytecodeOptimized(
                    enableBytecodePeephole: !rawBc,
                  ),
                ).compile(program)
                as LuaBytecodeArtifact;
        print(const LuaBytecodeDisassembler().render(artifact.chunk));
      }
    } catch (e, st) {
      stderr.writeln('Disassembly failed: $e');
      stderr.writeln(st);
      exit(1);
    }
    exit(0);
  }

  // Handle --compile (compile-only, no execution)
  if (argResults['compile'] as bool) {
    if (restArgs.isEmpty) {
      stderr.writeln('Error: --compile requires a script file argument.');
      exit(1);
    }
    final scriptPath = restArgs.first;
    // No standard extension for binary chunks — caller must name the file.
    final outputPath = argResults['output'] as String?;
    if (outputPath == null || outputPath.isEmpty) {
      stderr.writeln(
        'Error: --compile requires --output / -o <path> '
        '(no default extension; binary is detected by header).',
      );
      exit(1);
    }
    _compileToBytecode(
      scriptPath,
      outputPath,
      dartOutputPath: argResults['dart-output'] as String?,
      preserveDebug: argResults['preserve-debug'] as bool,
    );
    return;
  }
  // Handle --check-env (verify native compilation toolchain)
  if (argResults['check-env'] as bool) {
    stderr.writeln('Checking native compilation environment...');
    try {
      final r = await Process.run('dart', [
        'run',
        'tool/compile_llvm.dart',
        '--help',
      ]);
      if (r.exitCode == 0) {
        stderr.writeln('  compile_llvm.dart: OK (${r.exitCode})');
      }
    } catch (_) {
      stderr.writeln('  compile_llvm.dart: not found in tool/');
    }
    for (final tool in ['zig', 'llc', 'clang']) {
      try {
        final r = await Process.run('which', [tool]);
        stdout.writeln('  $tool: ${r.stdout.toString().trim()}');
      } catch (_) {
        stderr.writeln('  $tool: not found');
      }
    }
    exit(0);
  }

  // Handle --native (LLVM native compilation)
  if (argResults['native'] as bool) {
    if (restArgs.isEmpty) {
      stderr.writeln('Error: --native requires a script file argument.');
      exit(1);
    }
    final scriptPath = restArgs.first;
    if (!File(scriptPath).existsSync()) {
      stderr.writeln('File not found: $scriptPath');
      exit(1);
    }
    final outputPath = argResults['output'] as String?;
    if (outputPath == null || outputPath.isEmpty) {
      stderr.writeln('Error: --native requires --output / -o <path>');
      exit(1);
    }
    await checkEnvironment();
    final out = await compileLuaToNative(
      scriptPath: scriptPath,
      outputPath: outputPath,
    );
    stderr.writeln('Done: $out');
    stderr.writeln('Run: $out');
    exit(0);
  }

  BaseCommand.resetBridge();

  final emitDocsFormat = argResults['emit-docs'] as String?;
  if (emitDocsFormat != null) {
    await _emitDocs(
      format: emitDocsFormat,
      output: argResults['emit-docs-output'] as String?,
    );
    return;
  }

  // Handle debug mode
  if (argResults['debug'] as bool) {
    debugMode = true;
    io.writeln('Debug mode enabled');
  }

  // Set up logging
  final logLevel = argResults['level'] as String?;
  final logCategories =
      (argResults['category'] as List<String>?) ?? const <String>[];

  ctx.Level? cliLevel;
  if (logLevel != null && logLevel.isNotEmpty) {
    cliLevel = parseLogLevel(logLevel) ?? ctx.Level.warning;
  }

  setLualikeLogging(
    enabled: debugMode,
    level: cliLevel,
    categories: logCategories.isEmpty ? null : logCategories,
  );

  // Handle special case where no arguments provided
  if (args.isEmpty) {
    if (stdin.hasTerminal) {
      // Terminal mode: show version and enter interactive mode
      final versionCmd = VersionCommand();
      await versionCmd.run();
      final interactiveCmd = InteractiveCommandWrapper(debugMode: debugMode);
      await interactiveCmd.run();
    } else {
      // Non-terminal mode: read from stdin
      final stdinCmd = StdinCommand([], args.toList());
      await stdinCmd.run();
    }
    return;
  }

  // Handle special case for single '-' argument (stdin)
  if (args.length == 1 && args.first == '-') {
    final stdinCmd = StdinCommand([], args.toList());
    await stdinCmd.run();
    return;
  }

  // Handle LUA_INIT using BaseCommand functionality
  final baseCmd = _LuaInitCommand();
  await baseCmd.handleLuaInit();

  // Handle require files (-l)
  final requireFiles = argResults['require'] as List<String>;
  for (final file in requireFiles) {
    final requireCmd = RequireCommand(file, args.toList());
    await requireCmd.run();
  }

  // Handle execute strings (-e)
  final executeStrings = argResults['execute'] as List<String>;
  for (final code in executeStrings) {
    final executeCmd = ExecuteCommand(code, args.toList());
    await executeCmd.run();
  }

  // Handle --lua-test (runs a Lua test suite file with standard env)
  final luaTest = argResults['lua-test'] as String?;
  if (luaTest != null) {
    final testFile = luaTest.endsWith('.lua') ? luaTest : '$luaTest.lua';
    final testPath = 'luascripts/test/$testFile';
    final initCode =
        '_port = true; _soft = true; '
        "package.path = 'luascripts/test/?.lua;luascripts/test/?/init.lua;?.lua;;'; "
        "dofile('$testPath')";
    final executeCmd = ExecuteCommand(initCode, args.toList());
    await executeCmd.run();
  }

  // Handle script file and arguments
  if (restArgs.isNotEmpty) {
    final scriptPath = restArgs.first;
    final scriptArgs = restArgs.skip(1).toList();
    final scriptCmd = ScriptCommand(scriptPath, scriptArgs, args.toList());
    await scriptCmd.run();
  }

  // Handle interactive mode (-i)
  if (argResults['interactive'] as bool) {
    final interactiveCmd = InteractiveCommandWrapper(debugMode: debugMode);
    await interactiveCmd.run();
  }
}