diff --git a/pom.xml b/pom.xml index ba9e035..edc8a5d 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,7 @@ - org.jcnc.snow.compiler.cli.SnowCompiler + org.jcnc.snow.compiler.cli.SnowCLI true diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 4759b93..3f1c505 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,4 +1,5 @@ module org.jcnc.snow.compiler { + uses org.jcnc.snow.compiler.cli.CLICommand; requires java.desktop; requires java.logging; exports org.jcnc.snow.compiler.ir.core; diff --git a/src/main/java/org/jcnc/snow/compiler/cli/CLICommand.java b/src/main/java/org/jcnc/snow/compiler/cli/CLICommand.java new file mode 100644 index 0000000..4e342af --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/cli/CLICommand.java @@ -0,0 +1,44 @@ +package org.jcnc.snow.compiler.cli; + +/** + *

+ * 所有子命令(如 compile、run 等)都必须实现的命令接口。 + *

+ * + *

+ + */ +public interface CLICommand { + + /** + * 获取命令的名称(如 "compile"、"run")。 + * + * @return 命令名字符串 + */ + String name(); + + /** + * 获取命令的一行简介(用于 help 列表)。 + * + * @return 命令描述字符串 + */ + String description(); + + /** + * 打印命令的专用 usage 信息(可选实现)。 + * 可覆盖此方法自定义帮助信息,默认无操作。 + */ + default void printUsage() {} + + /** + * 执行命令逻辑。 + * + * @param args 传递给子命令的参数(不含命令名本身) + * @return 进程退出码(0 为成功,非0为错误) + * @throws Exception 可抛出任意异常,框架会统一捕获和输出 + */ + int execute(String[] args) throws Exception; +} diff --git a/src/main/java/org/jcnc/snow/compiler/cli/SnowCLI.java b/src/main/java/org/jcnc/snow/compiler/cli/SnowCLI.java new file mode 100644 index 0000000..1d1dbf4 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/cli/SnowCLI.java @@ -0,0 +1,120 @@ +package org.jcnc.snow.compiler.cli; + +import org.jcnc.snow.compiler.cli.commands.CompileCommand; +import org.jcnc.snow.compiler.cli.commands.RunCommand; + +import java.util.*; + +/** + *

+ * Snow 语言统一命令行入口(CLI)。 + *
+ * 负责命令注册、解析与调度。 + *

+ *
+ * 示例用法:
+ *   $ snow help
+ *   $ snow compile main.snow
+ *   $ snow run     main.vm
+ * 
+ * + */ +public final class SnowCLI { + + /** 命令注册表,按插入顺序保存命令名到实现的映射。 */ + private final Map registry = new LinkedHashMap<>(); + + /** + * 构造 CLI,自动注册所有可用命令。 + * + */ + public SnowCLI() { + // 1. 自动发现 ServiceLoader 扩展命令 + ServiceLoader.load(CLICommand.class).forEach(this::register); + // 2. 注册核心命令,保证 CLI 可用 + register(new CompileCommand()); + register(new RunCommand()); + } + + /** + * 注册一个命令到 CLI(命令名唯一,若已注册则跳过)。 + * + * @param cmd 待注册命令 + */ + private void register(CLICommand cmd) { + registry.putIfAbsent(cmd.name(), cmd); + } + + /** + * 解析命令行参数并执行相应命令。 + * + * @param args 命令行参数 + * @return 进程退出码(0=成功, 1=未知命令, -1=命令异常) + */ + public int run(String[] args) { + // 无参数或 help,打印全局用法 + if (args.length == 0 + || Set.of("help", "-h", "--help").contains(args[0])) { + printGlobalUsage(); + return 0; + } + + // 根据命令名查找注册表 + CLICommand cmd = registry.get(args[0]); + if (cmd == null) { + System.err.printf("Unknown command: %s%n%n", args[0]); + printGlobalUsage(); + return 1; + } + + // 提取命令余下参数(不包含命令名) + String[] sub = Arrays.copyOfRange(args, 1, args.length); + try { + return cmd.execute(sub); + } catch (Exception e) { + System.err.printf("Error executing command '%s': %s%n", + cmd.name(), e.getMessage()); + e.printStackTrace(System.err); + return -1; + } + } + + /** + * 打印全局帮助信息(所有已注册命令的 usage)。 + */ + private void printGlobalUsage() { + System.out.println(""" + Snow Programming Language CLI + Usage: snow [options] + + Available commands:"""); + int pad = registry.keySet().stream() + .mapToInt(String::length).max().orElse(10) + 2; + registry.values().stream() + .sorted(Comparator.comparing(CLICommand::name)) + .forEach(c -> System.out.printf(" %-" + pad + "s%s%n", + c.name(), c.description())); + + System.out.println(""" + + Use 'snow --help' for command-specific details. + """); + } + + /** + * CLI 程序主入口。 + * + * @param args 命令行参数 + */ + public static void main(String[] args) { + int code = new SnowCLI().run(args); + System.exit(code); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/cli/SnowCompiler.java b/src/main/java/org/jcnc/snow/compiler/cli/SnowCompiler.java deleted file mode 100644 index 512da03..0000000 --- a/src/main/java/org/jcnc/snow/compiler/cli/SnowCompiler.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.jcnc.snow.compiler.cli; - -import org.jcnc.snow.compiler.backend.alloc.RegisterAllocator; -import org.jcnc.snow.compiler.backend.builder.VMCodeGenerator; -import org.jcnc.snow.compiler.backend.builder.VMProgramBuilder; -import org.jcnc.snow.compiler.backend.core.InstructionGenerator; -import org.jcnc.snow.compiler.backend.generator.InstructionGeneratorProvider; -import org.jcnc.snow.compiler.ir.builder.IRProgramBuilder; -import org.jcnc.snow.compiler.ir.core.IRFunction; -import org.jcnc.snow.compiler.ir.core.IRInstruction; -import org.jcnc.snow.compiler.ir.core.IRProgram; -import org.jcnc.snow.compiler.ir.value.IRVirtualRegister; -import org.jcnc.snow.compiler.lexer.core.LexerEngine; -import org.jcnc.snow.compiler.parser.ast.base.Node; -import org.jcnc.snow.compiler.parser.context.ParserContext; -import org.jcnc.snow.compiler.parser.core.ParserEngine; -import org.jcnc.snow.compiler.parser.function.ASTPrinter; -import org.jcnc.snow.compiler.semantic.core.SemanticAnalyzerRunner; -import org.jcnc.snow.vm.engine.VMMode; -import org.jcnc.snow.vm.engine.VirtualMachineEngine; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; - -/** - * SnowCompiler CLI —— 多文件 / 单文件 / 目录 模式。 - */ -public class SnowCompiler { - - public static void main(String[] args) throws IOException { - if (args.length == 0) { - System.err.println(""" - Usage: - snow [file2.snow …] - snow -d (compile all *.snow recursively) - """); - return; - } - - /* ---------- 1. 收集所有待编译源码 ---------- */ - List sources = collectSources(args); - if (sources.isEmpty()) { - System.err.println("No .snow source files found."); - return; - } - - /* ---------- 2. 逐个词法+语法分析,合并 AST ---------- */ - List allAst = new ArrayList<>(); - for (Path p : sources) { - if (!Files.exists(p)) { - System.err.println("File not found: " + p); - return; - } - String code = Files.readString(p, StandardCharsets.UTF_8); - - // 保持原有“## 源代码”打印,但标注文件名,兼容旧脚本 - System.out.println("## 源代码 (" + p.getFileName() + ")"); - System.out.println(code); - - LexerEngine lexer = new LexerEngine(code, p.toString()); - ParserContext ctx = new ParserContext(lexer.getAllTokens(), p.toString()); - allAst.addAll(new ParserEngine(ctx).parse()); - } - - /* ---------- 3. 语义分析 ---------- */ - SemanticAnalyzerRunner.runSemanticAnalysis(allAst, false); - - /* ---------- 4. AST → IR ---------- */ - IRProgram program = new IRProgramBuilder().buildProgram(allAst); - program = reorderForEntry(program); // 保证入口 main 在首位 - - System.out.println("## 编译器输出"); - System.out.println("### AST"); - ASTPrinter.printJson(allAst); - System.out.println("### IR"); - System.out.println(program); - - /* ---------- 5. IR → VM 指令 ---------- */ - VMProgramBuilder builder = new VMProgramBuilder(); - List> generators = InstructionGeneratorProvider.defaultGenerators(); - - for (IRFunction fn : program.functions()) { - Map slotMap = - new RegisterAllocator().allocate(fn); - new VMCodeGenerator(slotMap, builder, generators).generate(fn); - } - List finalCode = builder.build(); - - System.out.println("### VM code"); - finalCode.forEach(System.out::println); - - /* ---------- 6. 运行虚拟机 ---------- */ - VirtualMachineEngine vm = new VirtualMachineEngine(VMMode.RUN); - vm.execute(finalCode); - vm.printLocalVariables(); - } - - - /** - * 根据参数收集待编译文件: - * - snow file1 file2 … ← 多文件 / 单文件 - * - snow -d srcDir ← 目录递归所有 *.snow - */ - private static List collectSources(String[] args) throws IOException { - if (args.length == 2 && "-d".equals(args[0])) { - Path dir = Path.of(args[1]); - if (!Files.isDirectory(dir)) { - System.err.println("Not a directory: " + dir); - return List.of(); - } - try (var stream = Files.walk(dir)) { - return stream.filter(p -> p.toString().endsWith(".snow")) - .sorted() // 稳定顺序,方便比对输出 - .toList(); - } - } - // 普通文件参数 - return Arrays.stream(args).map(Path::of).toList(); - } - - /** - * 把 main 函数放到 Program.functions()[0],保证 PC=0 即入口; - * 如果用户未写 main,则保持原顺序(语义分析会报错)。 - */ - private static IRProgram reorderForEntry(IRProgram in) { - List ordered = new ArrayList<>(in.functions()); - int idx = -1; - for (int i = 0; i < ordered.size(); i++) { - if ("main".equals(ordered.get(i).name())) { - idx = i; - break; - } - } - if (idx > 0) Collections.swap(ordered, 0, idx); - - IRProgram out = new IRProgram(); - ordered.forEach(out::add); - return out; - } -} diff --git a/src/main/java/org/jcnc/snow/compiler/cli/commands/CompileCommand.java b/src/main/java/org/jcnc/snow/compiler/cli/commands/CompileCommand.java new file mode 100644 index 0000000..cfa7e36 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/cli/commands/CompileCommand.java @@ -0,0 +1,233 @@ +package org.jcnc.snow.compiler.cli.commands; + +import org.jcnc.snow.compiler.cli.CLICommand; +import org.jcnc.snow.compiler.backend.alloc.RegisterAllocator; +import org.jcnc.snow.compiler.backend.builder.VMCodeGenerator; +import org.jcnc.snow.compiler.backend.builder.VMProgramBuilder; +import org.jcnc.snow.compiler.backend.core.InstructionGenerator; +import org.jcnc.snow.compiler.backend.generator.InstructionGeneratorProvider; +import org.jcnc.snow.compiler.ir.builder.IRProgramBuilder; +import org.jcnc.snow.compiler.ir.core.IRFunction; +import org.jcnc.snow.compiler.ir.core.IRInstruction; +import org.jcnc.snow.compiler.ir.core.IRProgram; +import org.jcnc.snow.compiler.ir.value.IRVirtualRegister; +import org.jcnc.snow.compiler.lexer.core.LexerEngine; +import org.jcnc.snow.compiler.parser.ast.base.Node; +import org.jcnc.snow.compiler.parser.context.ParserContext; +import org.jcnc.snow.compiler.parser.core.ParserEngine; +import org.jcnc.snow.compiler.parser.function.ASTPrinter; +import org.jcnc.snow.compiler.semantic.core.SemanticAnalyzerRunner; +import org.jcnc.snow.vm.VMLauncher; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +/** + *

+ * 编译器命令行入口:实现 `snow compile` 命令。 + * 支持编译一个或多个 .snow 源文件为 VM 字节码文件,或编译后直接运行。 + *

+ *
    + *
  • 支持编译单个文件、多个文件、或目录递归批量编译。
  • + *
  • 支持 compile-only 或 compile-then-run 模式。
  • + *
  • 完成词法、语法、语义分析,IR 构建,寄存器分配,VM 指令生成。
  • + *
+ *

+ * 用法:
+ * snow compile foo.snow
+ * snow compile -d src
+ * snow compile run foo.snow + *

+ */ +public final class CompileCommand implements CLICommand { + + /** + * 获取命令名称。 + * + * @return 命令名 "compile" + */ + @Override + public String name() { return "compile"; } + + /** + * 获取命令的简要描述。 + * + * @return 命令描述文本 + */ + @Override + public String description() { + return "Compile .snow source files into VM byte-code."; + } + + /** + * 打印用法说明。 + */ + @Override + public void printUsage() { + System.out.println(""" + Usage: + snow compile [file2.snow …] (compile only) + snow compile -d (recursively compile) + snow compile run […] (compile then run) + """); + } + + /** + * 执行 compile 命令。 + * + * @param args 命令行参数 + * @return 0 表示成功,非0表示错误 + * @throws Exception 发生任何编译、文件、运行错误 + */ + @Override + public int execute(String[] args) throws Exception { + // 0. 检查是否包含 run + boolean runAfterCompile = false; + int offset = 0; + if (args.length > 0 && "run".equals(args[0])) { + runAfterCompile = true; + offset = 1; + } + if (args.length - offset == 0) { + printUsage(); + return 1; + } + String[] srcArgs = Arrays.copyOfRange(args, offset, args.length); + + List sources = collectSources(srcArgs); + if (sources.isEmpty()) { + System.err.println("No .snow source files found."); + return 1; + } + + // 1. 词法+语法分析,合并AST + List allAst = new ArrayList<>(); + for (Path p : sources) { + if (!Files.exists(p)) { + System.err.println("File not found: " + p); + return 1; + } + String code = Files.readString(p, StandardCharsets.UTF_8); + LexerEngine lexer = new LexerEngine(code, p.toString()); + ParserContext ctx = new ParserContext(lexer.getAllTokens(), p.toString()); + allAst.addAll(new ParserEngine(ctx).parse()); + } + + // 2. 语义分析 + SemanticAnalyzerRunner.runSemanticAnalysis(allAst, false); + + // 3. AST -> IR + IRProgram program = new IRProgramBuilder().buildProgram(allAst); + program = reorderForEntry(program); // 保证入口 main 在首位 + + System.out.println("## 编译器输出"); + System.out.println("### AST"); + ASTPrinter.printJson(allAst); + System.out.println("### IR"); + System.out.println(program); + + // 4. IR -> VM指令 + VMProgramBuilder builder = new VMProgramBuilder(); + List> generators = + InstructionGeneratorProvider.defaultGenerators(); + + for (IRFunction fn : program.functions()) { + Map slotMap = + new RegisterAllocator().allocate(fn); + new VMCodeGenerator(slotMap, builder, generators).generate(fn); + } + List finalCode = builder.build(); + + System.out.println("### VM code"); + finalCode.forEach(System.out::println); + + // 5. 将VM指令写入文件 + Path outputFile = deriveOutputPath(sources); + Files.write(outputFile, finalCode, StandardCharsets.UTF_8); + System.out.println("Written to " + outputFile.toAbsolutePath()); + + // 6. 若指定run,立即执行 + if (runAfterCompile) { + System.out.println("\n=== Launching VM ==="); + VMLauncher.main(new String[]{outputFile.toString()}); + } + return 0; + } + + /** + * 收集所有待编译的源文件。 + *
    + *
  • 参数 "-d ":递归收集目录下所有 .snow 文件
  • + *
  • 否则认为是文件列表参数
  • + *
+ * + * @param args 命令行参数 + * @return 源文件路径列表,若未找到返回空列表 + * @throws IOException 文件IO错误 + */ + private static List collectSources(String[] args) throws IOException { + if (args.length == 2 && "-d".equals(args[0])) { + Path dir = Path.of(args[1]); + if (!Files.isDirectory(dir)) { + System.err.println("Not a directory: " + dir); + return List.of(); + } + try (var stream = Files.walk(dir)) { + return stream.filter(p -> p.toString().endsWith(".snow")) + .sorted() // 稳定顺序,方便比对输出 + .toList(); + } + } + // 普通文件参数 + return Arrays.stream(args).map(Path::of).toList(); + } + + /** + * 保证 main 函数排在 Program.functions()[0],使 PC=0 即为程序入口。 + *
    + *
  • 若存在 main,则与第0个函数互换
  • + *
  • 若不存在 main,则顺序不变(后续语义分析会报错)
  • + *
+ * + * @param in 原始IR程序对象 + * @return 处理后的IR程序对象 + */ + private static IRProgram reorderForEntry(IRProgram in) { + List ordered = new ArrayList<>(in.functions()); + int idx = -1; + for (int i = 0; i < ordered.size(); i++) { + if ("main".equals(ordered.get(i).name())) { + idx = i; + break; + } + } + if (idx > 0) Collections.swap(ordered, 0, idx); + + IRProgram out = new IRProgram(); + ordered.forEach(out::add); + return out; + } + + /** + * 推断输出文件名。 + *
    + *
  • 单文件编译:输出同名 .vm 文件
  • + *
  • 多文件/目录编译:输出 "program.vm" 到当前目录
  • + *
+ * + * @param sources 输入源文件路径列表 + * @return 输出的 VM 文件路径 + */ + private static Path deriveOutputPath(List sources) { + if (sources.size() == 1) { + Path src = sources.getFirst(); + String name = src.getFileName().toString() + .replaceFirst("\\.snow$", ""); + return src.resolveSibling(name + ".vm"); + } + return Path.of("program.vm"); + } +} diff --git a/src/main/java/org/jcnc/snow/compiler/cli/commands/RunCommand.java b/src/main/java/org/jcnc/snow/compiler/cli/commands/RunCommand.java new file mode 100644 index 0000000..fb737d4 --- /dev/null +++ b/src/main/java/org/jcnc/snow/compiler/cli/commands/RunCommand.java @@ -0,0 +1,68 @@ +package org.jcnc.snow.compiler.cli.commands; + +import org.jcnc.snow.compiler.cli.CLICommand; +import org.jcnc.snow.vm.VMLauncher; + +/** + *

+ * 命令实现:`snow run` + *
+ * 用于运行已编译的 VM 字节码文件(.vm)。 + *

+ *
    + *
  • 支持传递额外 VM 参数。
  • + *
  • 实际执行通过 {@link VMLauncher#main(String[])} 入口完成。
  • + *
+ *

+ * 用法:
+ * snow run program.vm + *

+ */ +public final class RunCommand implements CLICommand { + + /** + * 获取命令名。 + * + * @return "run" + */ + @Override + public String name() { return "run"; } + + /** + * 获取命令描述。 + * + * @return 命令简介 + */ + @Override + public String description() { + return "Execute compiled VM instructions."; + } + + /** + * 打印该命令的用法说明。 + */ + @Override + public void printUsage() { + System.out.println(""" + Usage: snow run [additional VM options] + """); + } + + /** + * 执行 run 命令,运行 VM 指令文件。 + * + * @param args 剩余参数(不含命令名),第一个应为 .vm 文件路径 + * @return 0 表示成功,1 表示参数错误 + * @throws Exception 运行 VM 时可能抛出的异常 + */ + @Override + public int execute(String[] args) throws Exception { + if (args.length == 0) { + printUsage(); + return 1; + } + // 直接复用 VM 启动逻辑 + VMLauncher.main(args); + return 0; + } +}