diff --git a/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md index 25132b9..711fceb 100644 --- a/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md +++ b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md @@ -20,7 +20,7 @@ https://gitee.com/jcnc-org/snow/blob/main/doc/Git-Management/Git-Management.md 感谢你的配合!🙏 --> -# 描述 (Description) +## 描述 (Description) 请简要描述本次变更的目的和内容。 diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index aa5369c..3f8a7b3 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,5 +1,7 @@ +import org.jcnc.snow.cli.api.CLICommand; + module org.jcnc.snow.compiler { - uses org.jcnc.snow.cli.CLICommand; + uses CLICommand; requires java.desktop; requires java.logging; exports org.jcnc.snow.compiler.ir.core; diff --git a/src/main/java/org/jcnc/snow/cli/SnowCLI.java b/src/main/java/org/jcnc/snow/cli/SnowCLI.java index 5fae2a7..03a4391 100644 --- a/src/main/java/org/jcnc/snow/cli/SnowCLI.java +++ b/src/main/java/org/jcnc/snow/cli/SnowCLI.java @@ -1,8 +1,7 @@ package org.jcnc.snow.cli; -import org.jcnc.snow.cli.commands.CompileCommand; -import org.jcnc.snow.cli.commands.RunCommand; -import org.jcnc.snow.cli.commands.VersionCommand; +import org.jcnc.snow.cli.api.CLICommand; +import org.jcnc.snow.cli.commands.*; import org.jcnc.snow.cli.utils.CLIUtils; import org.jcnc.snow.cli.utils.VersionUtils; @@ -30,14 +29,16 @@ public class SnowCLI { * 值为返回相应 {@link CLICommand} 实例的 Supplier。 */ private static final Map> COMMANDS = Map.of( + "generate", GenerateCommand::new, "compile", CompileCommand::new, "run", RunCommand::new, "version", VersionCommand::new, - "init", org.jcnc.snow.cli.commands.InitCommand::new, - "build", org.jcnc.snow.cli.commands.BuildCommand::new, - "install", org.jcnc.snow.cli.commands.InstallCommand::new, - "publish", org.jcnc.snow.cli.commands.PublishCommand::new, - "clean", org.jcnc.snow.cli.commands.CleanCommand::new + "init", InitCommand::new, + "build", BuildCommand::new, + "install", InstallCommand::new, + "publish", PublishCommand::new, + "clean", CleanCommand::new + ); /** diff --git a/src/main/java/org/jcnc/snow/cli/CLICommand.java b/src/main/java/org/jcnc/snow/cli/api/CLICommand.java similarity index 94% rename from src/main/java/org/jcnc/snow/cli/CLICommand.java rename to src/main/java/org/jcnc/snow/cli/api/CLICommand.java index 0874cf1..71e227e 100644 --- a/src/main/java/org/jcnc/snow/cli/CLICommand.java +++ b/src/main/java/org/jcnc/snow/cli/api/CLICommand.java @@ -1,4 +1,6 @@ -package org.jcnc.snow.cli; +package org.jcnc.snow.cli.api; + +import org.jcnc.snow.cli.SnowCLI; /** * 所有 CLI 子命令(如 compile、run 等)都必须实现的命令接口。 diff --git a/src/main/java/org/jcnc/snow/cli/commands/BuildCommand.java b/src/main/java/org/jcnc/snow/cli/commands/BuildCommand.java index f8b9c45..17e1c0f 100644 --- a/src/main/java/org/jcnc/snow/cli/commands/BuildCommand.java +++ b/src/main/java/org/jcnc/snow/cli/commands/BuildCommand.java @@ -1,6 +1,6 @@ package org.jcnc.snow.cli.commands; -import org.jcnc.snow.cli.CLICommand; +import org.jcnc.snow.cli.api.CLICommand; import org.jcnc.snow.pkg.dsl.CloudDSLParser; import org.jcnc.snow.pkg.lifecycle.LifecycleManager; import org.jcnc.snow.pkg.lifecycle.LifecyclePhase; diff --git a/src/main/java/org/jcnc/snow/cli/commands/CleanCommand.java b/src/main/java/org/jcnc/snow/cli/commands/CleanCommand.java index 8f42b65..0c65244 100644 --- a/src/main/java/org/jcnc/snow/cli/commands/CleanCommand.java +++ b/src/main/java/org/jcnc/snow/cli/commands/CleanCommand.java @@ -1,6 +1,6 @@ package org.jcnc.snow.cli.commands; -import org.jcnc.snow.cli.CLICommand; +import org.jcnc.snow.cli.api.CLICommand; import org.jcnc.snow.pkg.lifecycle.LifecycleManager; import org.jcnc.snow.pkg.lifecycle.LifecyclePhase; import org.jcnc.snow.pkg.tasks.CleanTask; diff --git a/src/main/java/org/jcnc/snow/cli/commands/CompileCommand.java b/src/main/java/org/jcnc/snow/cli/commands/CompileCommand.java index 8d77e8d..993e1cb 100644 --- a/src/main/java/org/jcnc/snow/cli/commands/CompileCommand.java +++ b/src/main/java/org/jcnc/snow/cli/commands/CompileCommand.java @@ -1,48 +1,35 @@ package org.jcnc.snow.cli.commands; -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.cli.CLICommand; -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 org.jcnc.snow.cli.api.CLICommand; +import org.jcnc.snow.pkg.dsl.CloudDSLParser; +import org.jcnc.snow.pkg.model.Project; +import org.jcnc.snow.pkg.tasks.CompileTask; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; - +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** - * CLI 命令:将 .snow 源文件编译为 VM 字节码(.water 文件)。 - *

- * 支持递归目录、多文件编译,可选编译后立即运行。
- * 命令参数支持 run、-o、-d 等。 - *

+ * CLI 命令:编译当前项目。 * - *
- * 用法示例:
- * $ snow compile [run] [-o <name>] [-d <srcDir>] [file1.snow file2.snow …]
- * 
+ *

工作模式说明:

+ * + * + *

两种模式均将最终参数交由 {@link CompileTask} 处理。

*/ public final class CompileCommand implements CLICommand { - /* --------------------------------------------------------------------- */ - /* CLICommand 接口实现 */ - /* --------------------------------------------------------------------- */ - @Override public String name() { return "compile"; @@ -56,202 +43,50 @@ public final class CompileCommand implements CLICommand { @Override public void printUsage() { System.out.println("Usage:"); - System.out.println(" snow compile [run] [-o ] [-d ] [file1.snow file2.snow …]"); - System.out.println("Options:"); - System.out.println(" run compile then run"); - System.out.println(" -o specify output base name (without .water suffix)"); - System.out.println(" -d recursively compile all .snow files in directory"); + System.out.println(" snow compile [run] (cloud mode, use project.cloud)"); + System.out.println(" snow compile [run] [-o ] [-d ] [file1.snow …] (GOPATH mode)"); } - /* --------------------------------------------------------------------- */ - /* 核心:执行 compile 子命令 */ - /* --------------------------------------------------------------------- */ - @Override public int execute(String[] args) throws Exception { - /* ---------------- 解析命令行参数 ---------------- */ - boolean runAfterCompile = false; - String outputName = null; - Path dir = null; - List sources = new ArrayList<>(); - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - switch (arg) { - case "run" -> runAfterCompile = true; - case "-o" -> { - if (i + 1 < args.length) outputName = args[++i]; - else { - System.err.println("Missing argument for -o"); - printUsage(); - return 1; - } - } - case "-d" -> { - if (i + 1 < args.length) dir = Path.of(args[++i]); - else { - System.err.println("Missing argument for -d"); - printUsage(); - return 1; - } - } - default -> { - if (arg.endsWith(".snow")) { - sources.add(Path.of(arg)); - } else { - System.err.println("Unknown option or file: " + arg); - printUsage(); - return 1; - } + Path dslFile = Paths.get("project.cloud"); + Project project; + String[] compileArgs; + + /* ---------- 1. Cloud 模式 ---------- */ + if (Files.exists(dslFile)) { + project = CloudDSLParser.parse(dslFile); + + List argList = new ArrayList<>(); + + // 保留用户在 cloud 模式下传入的 “run” 标志 + for (String a : args) { + if ("run".equals(a)) { + argList.add("run"); } } + + /* 源码目录:build.srcDir -> 默认 src */ + String srcDir = project.getBuild().get("srcDir", "src"); + argList.add("-d"); + argList.add(srcDir); + + /* 输出名称:build.output -> fallback to artifact */ + String output = project.getBuild().get("output", project.getArtifact()); + argList.add("-o"); + argList.add(output); + + compileArgs = argList.toArray(new String[0]); + } + /* ---------- 2. Local 模式 ---------- */ + else { + project = Project.fromFlatMap(Collections.emptyMap()); // 占位项目,保持接口统一 + compileArgs = args; // 透传原始 CLI 参数 } - /* --------- 如果指定了目录则递归收集所有 *.snow --------- */ - if (dir != null) { - if (!Files.isDirectory(dir)) { - System.err.println("Not a directory: " + dir); - return 1; - } - try (var stream = Files.walk(dir)) { - stream.filter(p -> p.toString().endsWith(".snow")) - .sorted() // 确保稳定顺序 - .forEach(sources::add); - } - } - - if (sources.isEmpty()) { - System.err.println("No .snow source files found."); - return 1; - } - - /* 多文件但未指定 -o 且非目录编译 —— 提示必须指定输出名 */ - if (sources.size() > 1 && outputName == null && dir == null) { - System.err.println("Please specify output name using -o "); - return 1; - } - - /* ----------------------------------------------------------------- */ - /* 1. 词法 + 语法分析;同时打印源代码 */ - /* ----------------------------------------------------------------- */ - List allAst = new ArrayList<>(); - - System.out.println("## 编译器输出"); - System.out.println("### Snow 源代码"); // ========== 新增:二级标题 ========== - - 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); - - // ------- 打印每个文件的源码 ------- - 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()); - } - - /* ----------------------------------------------------------------- */ - /* 2. 语义分析 */ - /* ----------------------------------------------------------------- */ - SemanticAnalyzerRunner.runSemanticAnalysis(allAst, false); - - /* ----------------------------------------------------------------- */ - /* 3. AST → IR,并把 main 函数调到首位 */ - /* ----------------------------------------------------------------- */ - IRProgram program = new IRProgramBuilder().buildProgram(allAst); - program = reorderForEntry(program); - - /* ---------------- 打印 AST / IR ---------------- */ - 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. 写出 .water 文件 */ - /* ----------------------------------------------------------------- */ - Path outputFile = deriveOutputPath(sources, outputName, dir); - Files.write(outputFile, finalCode, StandardCharsets.UTF_8); - System.out.println("Written to " + outputFile.toAbsolutePath()); - - /* ----------------------------------------------------------------- */ - /* 6. 可选:立即运行 VM */ - /* ----------------------------------------------------------------- */ - if (runAfterCompile) { - System.out.println("\n=== Launching VM ==="); - VMLauncher.main(new String[]{outputFile.toString()}); - } - + // 委托给 CompileTask 完成实际编译/运行 + new CompileTask(project, compileArgs).run(); return 0; } - - /* --------------------------------------------------------------------- */ - /* 辅助方法 */ - /* --------------------------------------------------------------------- */ - - /** - * 根据输入情况推断 .water 输出文件名: - *
    - *
  • 若指定 -o,则直接使用
  • - *
  • 目录编译:取目录名
  • - *
  • 单文件编译:取文件名去掉 .snow
  • - *
  • 其他情况兜底为 "program"
  • - *
- */ - private static Path deriveOutputPath(List sources, String outName, Path dir) { - String base; - if (outName != null) { - base = outName; - } else if (dir != null) { - base = dir.getFileName().toString(); - } else if (sources.size() == 1) { - base = sources.getFirst().getFileName().toString() - .replaceFirst("\\.snow$", ""); - } else { - base = "program"; - } - return Path.of(base + ".water"); - } - - /** - * 把 main 函数交换到程序函数列表首位,确保 PC=0 即入口。 - */ - private static IRProgram reorderForEntry(IRProgram in) { - List ordered = new ArrayList<>(in.functions()); - for (int i = 0; i < ordered.size(); i++) { - if ("main".equals(ordered.get(i).name())) { - Collections.swap(ordered, 0, i); - break; - } - } - IRProgram out = new IRProgram(); - ordered.forEach(out::add); - return out; - } } diff --git a/src/main/java/org/jcnc/snow/cli/commands/GenerateCommand.java b/src/main/java/org/jcnc/snow/cli/commands/GenerateCommand.java new file mode 100644 index 0000000..b83a28d --- /dev/null +++ b/src/main/java/org/jcnc/snow/cli/commands/GenerateCommand.java @@ -0,0 +1,87 @@ +package org.jcnc.snow.cli.commands; + +import org.jcnc.snow.cli.api.CLICommand; +import org.jcnc.snow.pkg.dsl.CloudDSLParser; +import org.jcnc.snow.pkg.lifecycle.LifecycleManager; +import org.jcnc.snow.pkg.lifecycle.LifecyclePhase; +import org.jcnc.snow.pkg.model.Project; +import org.jcnc.snow.pkg.tasks.GenerateTask; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * CLI 命令:根据 project.cloud 生成项目目录结构。 + *

+ * 负责解析云项目描述文件,并通过 {@link GenerateTask} + * 在 INIT 生命周期阶段内生成基础目录结构。 + *

+ * + *
+ * 用法示例:
+ * $ snow generate
+ * 
+ * + *

+ * 注意事项: + * - 若当前目录不存在 project.cloud,则提示用户先执行 `snow init`。 + * - 执行成功后,会输出已创建的目录/文件。 + *

+ */ +public final class GenerateCommand implements CLICommand { + + /** + * 返回命令名称,用于 CLI 调用。 + * + * @return 命令名称,如 "generate" + */ + @Override + public String name() { + return "generate"; + } + + /** + * 返回命令简介,用于 CLI 帮助或命令列表展示。 + * + * @return 命令描述字符串 + */ + @Override + public String description() { + return "Generate project directory structure based on project.cloud."; + } + + /** + * 打印命令用法信息。 + */ + @Override + public void printUsage() { + System.out.println("Usage: snow generate"); + } + + /** + * 执行生成任务。 + * + * @param args CLI 传入的参数数组(此命令不接受参数) + * @return 执行结果码(0 表示成功,1 表示 project.cloud 缺失) + * @throws Exception 执行过程中出现错误时抛出 + */ + @Override + public int execute(String[] args) throws Exception { + Path dsl = Paths.get("project.cloud"); + if (Files.notExists(dsl)) { + System.err.println("project.cloud not found. Please run `snow init` first."); + return 1; + } + + /* 1. 解析 DSL */ + Project project = CloudDSLParser.parse(dsl); + + /* 2. 执行生成任务 —— 复用 Lifecycle INIT 阶段 */ + LifecycleManager lm = new LifecycleManager(); + lm.register(LifecyclePhase.INIT, new GenerateTask(project)); + lm.executeAll(); + + return 0; + } +} diff --git a/src/main/java/org/jcnc/snow/cli/commands/InitCommand.java b/src/main/java/org/jcnc/snow/cli/commands/InitCommand.java index 62a80b3..d9cbf92 100644 --- a/src/main/java/org/jcnc/snow/cli/commands/InitCommand.java +++ b/src/main/java/org/jcnc/snow/cli/commands/InitCommand.java @@ -1,15 +1,16 @@ package org.jcnc.snow.cli.commands; -import org.jcnc.snow.cli.CLICommand; +import org.jcnc.snow.cli.api.CLICommand; +import org.jcnc.snow.cli.utils.ProjectCloudExample; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; /** - * CLI 命令:创建新的项目骨架。 + * CLI 命令:初始化项目配置文件。 *

- * 用于快速初始化标准目录结构和 DSL 配置文件(project.cloud)。 + * 用于快速生成 DSL 配置文件(project.cloud)。 *

* *
@@ -36,7 +37,7 @@ public final class InitCommand implements CLICommand {
      */
     @Override
     public String description() {
-        return "Initialize a new project skeleton with directory structure and project.cloud file.";
+        return "Initialize a new project with project.cloud file.";
     }
 
     /**
@@ -44,11 +45,11 @@ public final class InitCommand implements CLICommand {
      */
     @Override
     public void printUsage() {
-        System.out.println("Usage: snow init [--lang ]");
+        System.out.println("Usage: snow init");
     }
 
     /**
-     * 执行项目初始化流程,创建 src 目录和 DSL 配置文件。
+     * 执行项目初始化流程,仅创建 DSL 配置文件。
      *
      * @param args CLI 传入的参数数组
      * @return 执行结果码(0 表示成功)
@@ -56,19 +57,11 @@ public final class InitCommand implements CLICommand {
      */
     @Override
     public int execute(String[] args) throws Exception {
-        // 生成 skeleton `.cloud` 文件和 src 目录
+        // 仅生成 `.cloud` 文件
         Path dir = Paths.get(".").toAbsolutePath();
-        Files.createDirectories(dir.resolve("src"));
         Path dsl = dir.resolve("project.cloud");
         if (Files.notExists(dsl)) {
-            Files.writeString(dsl, """
-                    # Generated by snow init
-                    project {
-                        group = "com.example"
-                        artifact = "demo-app"
-                        version = "0.0.1-SNAPSHOT"
-                    }
-                    """);
+            Files.writeString(dsl, ProjectCloudExample.getProjectCloud());
             System.out.println("[init] created " + dsl);
         } else {
             System.out.println("[init] project.cloud already exists");
diff --git a/src/main/java/org/jcnc/snow/cli/commands/InstallCommand.java b/src/main/java/org/jcnc/snow/cli/commands/InstallCommand.java
index 7cd0a2b..6199602 100644
--- a/src/main/java/org/jcnc/snow/cli/commands/InstallCommand.java
+++ b/src/main/java/org/jcnc/snow/cli/commands/InstallCommand.java
@@ -1,6 +1,6 @@
 package org.jcnc.snow.cli.commands;
 
-import org.jcnc.snow.cli.CLICommand;
+import org.jcnc.snow.cli.api.CLICommand;
 import org.jcnc.snow.pkg.dsl.CloudDSLParser;
 import org.jcnc.snow.pkg.model.Project;
 import org.jcnc.snow.pkg.resolver.DependencyResolver;
diff --git a/src/main/java/org/jcnc/snow/cli/commands/PublishCommand.java b/src/main/java/org/jcnc/snow/cli/commands/PublishCommand.java
index 2073f91..658e799 100644
--- a/src/main/java/org/jcnc/snow/cli/commands/PublishCommand.java
+++ b/src/main/java/org/jcnc/snow/cli/commands/PublishCommand.java
@@ -1,6 +1,6 @@
 package org.jcnc.snow.cli.commands;
 
-import org.jcnc.snow.cli.CLICommand;
+import org.jcnc.snow.cli.api.CLICommand;
 import org.jcnc.snow.pkg.dsl.CloudDSLParser;
 import org.jcnc.snow.pkg.lifecycle.LifecycleManager;
 import org.jcnc.snow.pkg.lifecycle.LifecyclePhase;
diff --git a/src/main/java/org/jcnc/snow/cli/commands/RunCommand.java b/src/main/java/org/jcnc/snow/cli/commands/RunCommand.java
index 09a10d4..0aeffe6 100644
--- a/src/main/java/org/jcnc/snow/cli/commands/RunCommand.java
+++ b/src/main/java/org/jcnc/snow/cli/commands/RunCommand.java
@@ -1,25 +1,26 @@
 package org.jcnc.snow.cli.commands;
 
-import org.jcnc.snow.cli.CLICommand;
-import org.jcnc.snow.vm.VMLauncher;
+import org.jcnc.snow.cli.api.CLICommand;
+import org.jcnc.snow.pkg.tasks.RunTask;
 
 /**
  * CLI 命令:运行已编译的 VM 字节码文件(.water)。
  * 

- * 用于执行 VM 程序文件。支持传递额外 VM 参数,实际运行由 {@link VMLauncher#main(String[])} 完成。 + * 仅解析参数并委托给 {@link RunTask}, + * 将 VM 运行逻辑下沉至 pkg 层,保持 CLI 无状态、薄封装。 *

* *
  * 用法示例:
- * $ snow run program.water [additional VM options]
+ * $ snow run main.water
  * 
*/ public final class RunCommand implements CLICommand { /** - * 返回命令名,用于 CLI 调用。 + * 返回命令名称,用于 CLI 调用。 * - * @return 命令名称字符串("run") + * @return 命令名称,如 "run" */ @Override public String name() { @@ -33,25 +34,15 @@ public final class RunCommand implements CLICommand { */ @Override public String description() { - return "Execute compiled VM instructions."; + return "Run the compiled VM bytecode file (.water)"; } /** - * 打印该命令的用法说明。 - */ - @Override - public void printUsage() { - System.out.println(""" - Usage: snow run [additional VM options] - """); - } - - /** - * 执行 run 命令,运行指定的 VM 字节码文件。 + * 执行运行任务。 * - * @param args 剩余参数(不含命令名),第一个应为 .water 文件路径,其后为可选 VM 参数 - * @return 0 表示执行成功,1 表示参数错误 - * @throws Exception VM 启动或执行过程中可能抛出的异常 + * @param args CLI 传入的参数数组 + * @return 执行结果码(0 表示成功,非 0 表示失败) + * @throws Exception 执行过程中出现错误时抛出 */ @Override public int execute(String[] args) throws Exception { @@ -59,7 +50,17 @@ public final class RunCommand implements CLICommand { printUsage(); return 1; } - VMLauncher.main(args); + // 委托给 RunTask 执行字节码运行逻辑 + new RunTask(args).run(); return 0; } + + /** + * 打印命令用法信息。 + */ + @Override + public void printUsage() { + System.out.println("Usage:"); + System.out.println(" snow run "); + } } diff --git a/src/main/java/org/jcnc/snow/cli/commands/VersionCommand.java b/src/main/java/org/jcnc/snow/cli/commands/VersionCommand.java index c8f3af8..d2989b5 100644 --- a/src/main/java/org/jcnc/snow/cli/commands/VersionCommand.java +++ b/src/main/java/org/jcnc/snow/cli/commands/VersionCommand.java @@ -1,7 +1,7 @@ package org.jcnc.snow.cli.commands; -import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.cli.SnowCLI; +import org.jcnc.snow.cli.api.CLICommand; /** * CLI 子命令:输出当前 Snow 工具的版本号。 diff --git a/src/main/java/org/jcnc/snow/cli/utils/CLIUtils.java b/src/main/java/org/jcnc/snow/cli/utils/CLIUtils.java index 462ead8..ca36951 100644 --- a/src/main/java/org/jcnc/snow/cli/utils/CLIUtils.java +++ b/src/main/java/org/jcnc/snow/cli/utils/CLIUtils.java @@ -1,6 +1,6 @@ package org.jcnc.snow.cli.utils; -import org.jcnc.snow.cli.CLICommand; +import org.jcnc.snow.cli.api.CLICommand; import java.util.List; import java.util.Map; diff --git a/src/main/java/org/jcnc/snow/cli/utils/ProjectCloudExample.java b/src/main/java/org/jcnc/snow/cli/utils/ProjectCloudExample.java new file mode 100644 index 0000000..b51e126 --- /dev/null +++ b/src/main/java/org/jcnc/snow/cli/utils/ProjectCloudExample.java @@ -0,0 +1,31 @@ +package org.jcnc.snow.cli.utils; + +public class ProjectCloudExample { + /** + * 工具类构造方法,禁止实例化。 + */ + private ProjectCloudExample() { + // 工具类不允许实例化 + } + + /** + * 获取 main.snow 示例模块的内容字符串。 + * + * @return main.snow 示例模块的完整代码 + */ + public static String getProjectCloud() { + return """ + # Generated by snow init + project { + group = "com.example" + artifact = "demo-app" + version = "0.0.1-SNAPSHOT" + } + + build { + srcDir = "src" + output = "build/demo-app" + } + """; + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/doc/README.md b/src/main/java/org/jcnc/snow/pkg/doc/README.md new file mode 100644 index 0000000..ec50ed5 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/doc/README.md @@ -0,0 +1,64 @@ +# Snow Build - 包管理模块 + +> Snow 构建工具中的 **pkg** 子系统 —— 负责解析 `.cloud` 配置、解析依赖、 orchestrate 构建生命周期并执行各阶段任务 + +## 项目简介 + +**包管理模块(pkg)** 是 Snow 构建工具的关键组成部分,承担“从配置到产物” 的整条流水线: + +1. **DSL 解析**:读取并解析 `.cloud` 配置文件,生成统一的项目/依赖/构建配置模型; +2. **生命周期编排**:按 *INIT → RESOLVE\_DEPENDENCIES → COMPILE → PACKAGE → PUBLISH → CLEAN* 的顺序驱动构建流程; +3. **依赖解析与缓存**:按需下载缺失依赖并存入本地缓存,离线优先; +4. **任务执行**:在各生命周期阶段调用对应 `Task` 实现(清理、编译、打包、发布等); +5. **配置模板展开**:支持在配置中使用 `@{key}` 形式的占位符,并在构建前统一替换。 + +整个模块强调 **可扩展性** 与 **内聚职责**:DSL → Model、Lifecycle → Task、Resolver → Repository 各自解耦,可独立演进。 + +## 核心功能 + +| 功能 | 关键类 | 说明 | +|-----------------|----------------------------------------------------------|-------------------------------------------------| +| **CloudDSL 解析** | `CloudDSLParser` | 支持区块语法、嵌套 build 展平、注释过滤,解析为扁平 `Map` | +| **模型对象** | `Project / Dependency / Repository / BuildConfiguration` | 只读数据类,提供静态工厂方法 *fromFlatMap()* | +| **生命周期管理** | `LifecycleManager`, `LifecyclePhase` | 注册并顺序执行阶段任务,阶段可扩展 | +| **任务体系** | `Task` + `*Task` 实现 | Clean / Compile / Package / Publish 四大内置任务,易于新增 | +| **依赖解析器** | `DependencyResolver` | 支持本地缓存、HTTP 下载、URI 解析、断点续传(基于 NIO) | +| **模板变量替换** | `BuildConfiguration` | 在加载阶段把 `@{key}` 替换为外部属性值 | + +## 模块结构 + +``` +pkg/ + ├── dsl/ + │ └── CloudDSLParser.java // .cloud DSL 解析 + ├── lifecycle/ + │ ├── LifecycleManager.java // 阶段编排 + │ └── LifecyclePhase.java // 阶段枚举 + ├── model/ + │ ├── Project.java // 项目信息 + │ ├── BuildConfiguration.java // 构建配置(支持变量) + │ ├── Dependency.java // 依赖描述 + │ └── Repository.java // 仓库描述 + ├── resolver/ + │ └── DependencyResolver.java // 依赖解析与缓存 + └── tasks/ + ├── Task.java // 任务接口 + ├── CleanTask.java // 清理 + ├── CompileTask.java // 编译 + ├── PackageTask.java // 打包 + └── PublishTask.java // 发布 +``` + +## 典型流程概览 + +1. 解析配置 + +2. 注册任务并执行 + +3. 解析依赖(在 RESOLVE\_DEPENDENCIES 阶段内部) + +## 开发环境 + +* JDK 24 或更高版本 +* Maven 构建管理 +* 推荐 IDE:IntelliJ IDEA \ No newline at end of file diff --git a/src/main/java/org/jcnc/snow/pkg/dsl/CloudDSLParser.java b/src/main/java/org/jcnc/snow/pkg/dsl/CloudDSLParser.java index c707a65..301e11d 100644 --- a/src/main/java/org/jcnc/snow/pkg/dsl/CloudDSLParser.java +++ b/src/main/java/org/jcnc/snow/pkg/dsl/CloudDSLParser.java @@ -10,61 +10,61 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * CloudDSLParser — .cloud 配置文件解析器 - *

- * 作用: - * - 读取 Snow 构建工具自定义的 .cloud 文件 - * - 将内容转换为内存中的 {@link Project} 模型 - *

- * 解析规则(: - *

- * 1. 顶级区块(project、properties、repositories、dependencies、build) - * 以 “sectionName {” 开始,以 “}” 结束。 - *

- * 2. 区块内部识别 “key = value” 形式的赋值。 - *

- * 3. build 区块允许嵌套;内部键通过 “.” 展平, - * 例如 compile.enabled = true。 + * CloudDSL 配置文件解析器。 *

+ * 负责将自定义的 .cloud 构建配置文件解析为 {@link Project} 模型。 + *

+ * + *
    + *
  • 顶级区块(如 project、properties、repositories、dependencies、build)以 sectionName { 开始,以 } 结束
  • + *
  • 区块内部只识别 key = value 赋值,行尾可有 # 注释
  • + *
  • build 区块支持嵌套,内部键通过 . 展平,例如 compile.enabled = true
  • + *
  • 新增:"value"'value' 形式的字面量自动去引号,调用方得到的均是不含引号的裸字符串
  • + *
+ * + *
+ * 示例 .cloud 文件片段:
+ * project {
+ *   group = com.example
+ *   artifact = "demo-app"
+ *   version = 1.0.0
+ * }
+ * 
*/ public final class CloudDSLParser { - /* ---------- 正则表达式 ---------- */ - - /** - * 匹配 “sectionName {” 形式的行 - */ + /** 匹配 sectionName { 的行 */ private static final Pattern SECTION_HEADER = Pattern.compile("^(\\w+)\\s*\\{\\s*$"); /** - * 匹配 “key = value” 行。 - * value 允许空格,并忽略行尾 “# …” 注释。 + * 匹配 key = value 行,忽略行尾注释。 + * 使用非贪婪匹配 .*?,确保 value 内部允许出现空格或 =。 */ - private static final Pattern KEY_VALUE = Pattern.compile("^(\\w+)\\s*=\\s*([^#]+?)\\s*(?:#.*)?$"); + private static final Pattern KEY_VALUE = Pattern.compile("^(\\w+)\\s*=\\s*(.*?)\\s*(?:#.*)?$"); - /** - * 匹配仅包含 “}” 的行(可有前后空白) - */ + /** 匹配仅为 } 的行 */ private static final Pattern BLOCK_END = Pattern.compile("^}\\s*$"); - /** - * 私有构造,禁止实例化 - */ - private CloudDSLParser() { - } + /** 工具类禁止实例化 */ + private CloudDSLParser() {} /** - * 解析指定 .cloud 文件并生成 {@link Project} 对象。 + * 解析指定 .cloud 文件为 {@link Project} 对象。 + *
    + *
  • 遇到语法错误(括号不配对、无法识别的行)时抛出异常
  • + *
  • 支持嵌套区块和注释
  • + *
  • 对字面量自动去除成对单/双引号
  • + *
* - * @param path 文件路径 - * @return 解析后的 Project + * @param path .cloud 文件路径 + * @return 解析得到的 Project 实例 * @throws IOException 文件读取失败 - * @throws IllegalStateException 语法错误(如括号不匹配、未知语句) + * @throws IllegalStateException 文件内容格式非法或语法错误 */ public static Project parse(Path path) throws IOException { - Deque sectionStack = new ArrayDeque<>(); // 记录当前区块层级 - Map flatMap = new LinkedHashMap<>(); // 扁平化 key → value + Deque sectionStack = new ArrayDeque<>(); // 当前区块栈 + Map flatMap = new LinkedHashMap<>(); // 扁平化后的 key → value List lines = Files.readAllLines(path); @@ -73,19 +73,19 @@ public final class CloudDSLParser { lineNo++; String line = raw.trim(); - /* 1. 跳过空行和注释 */ + // 跳过空行和注释 if (line.isEmpty() || line.startsWith("#")) { continue; } - /* 2. 区块起始:sectionName { */ + // 区块起始 Matcher sec = SECTION_HEADER.matcher(line); if (sec.matches()) { sectionStack.push(sec.group(1)); continue; } - /* 3. 区块结束:} */ + // 区块结束 if (BLOCK_END.matcher(line).matches()) { if (sectionStack.isEmpty()) { throw new IllegalStateException("第 " + lineNo + " 行出现未配对的 '}'"); @@ -94,13 +94,14 @@ public final class CloudDSLParser { continue; } - /* 4. 键值对:key = value */ + // 键值对 Matcher kv = KEY_VALUE.matcher(line); if (kv.matches()) { String key = kv.group(1).trim(); String value = kv.group(2).trim(); + value = unquote(value); // 去除首尾成对引号 - /* 4.1 计算前缀(栈倒序,即从外到内) */ + // 计算区块前缀 String prefix = String.join(".", (Iterable) sectionStack::descendingIterator); if (!prefix.isEmpty()) { key = prefix + "." + key; @@ -110,16 +111,29 @@ public final class CloudDSLParser { continue; } - /* 5. 无法识别的行 */ + // 无法识别的行 throw new IllegalStateException("无法解析第 " + lineNo + " 行: " + raw); } - /* 6. 检查括号是否全部闭合 */ + // 检查区块是否全部闭合 if (!sectionStack.isEmpty()) { throw new IllegalStateException("文件结束但区块未闭合:" + sectionStack); } - /* 7. 构造 Project 模型 */ + // 构建 Project 模型 return Project.fromFlatMap(flatMap); } + + /** + * 如果字符串首尾包裹成对单引号或双引号,则去掉引号后返回;否则直接返回原字符串。 + */ + private static String unquote(String s) { + if (s == null || s.length() < 2) return s; + char first = s.charAt(0); + char last = s.charAt(s.length() - 1); + if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) { + return s.substring(1, s.length() - 1); + } + return s; + } } diff --git a/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecycleManager.java b/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecycleManager.java index 60f5ebc..6c1b5db 100644 --- a/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecycleManager.java +++ b/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecycleManager.java @@ -6,9 +6,10 @@ import java.util.EnumMap; import java.util.Map; /** - * 管理不同生命周期阶段与其对应任务的工具类。 + * 生命周期任务管理器。
+ * 用于管理不同生命周期阶段与其对应 {@link Task},并支持顺序执行所有已注册任务。 *

- * 可为每个 {@link LifecyclePhase} 注册对应的 {@link Task},并按阶段顺序执行所有任务。 + * 可为每个 {@link LifecyclePhase} 注册对应的 {@link Task},并在构建/部署流程中自动执行。 *

* *
@@ -20,9 +21,7 @@ import java.util.Map;
  */
 public final class LifecycleManager {
 
-    /**
-     * 存储生命周期阶段与对应任务的映射关系。
-     */
+    /** 生命周期阶段与对应任务的映射关系 */
     private final Map tasks = new EnumMap<>(LifecyclePhase.class);
 
     /**
@@ -45,11 +44,13 @@ public final class LifecycleManager {
 
     /**
      * 按 {@link LifecyclePhase} 声明顺序依次执行所有已注册任务。
-     * 

- * 未注册任务的阶段将被跳过。任务执行前会打印阶段名。 - *

+ *
    + *
  • 未注册任务的阶段会被自动跳过
  • + *
  • 每个任务执行前会输出当前阶段名
  • + *
  • 执行中遇到异常将立即抛出并终止后续执行
  • + *
* - * @throws Exception 若某个任务执行时抛出异常,将直接抛出并终止后续任务执行 + * @throws Exception 若某个任务执行时抛出异常,将直接抛出 */ public void executeAll() throws Exception { for (LifecyclePhase phase : LifecyclePhase.values()) { diff --git a/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecyclePhase.java b/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecyclePhase.java index aaa464c..39d9759 100644 --- a/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecyclePhase.java +++ b/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecyclePhase.java @@ -1,7 +1,10 @@ package org.jcnc.snow.pkg.lifecycle; /** - * 定义了典型软件包生命周期的各个阶段。 + * 定义典型软件包生命周期的各个阶段枚举。 + *

+ * 用于区分构建、依赖、发布等不同阶段的任务调度与管理。 + *

*/ public enum LifecyclePhase { /** 初始化阶段 */ diff --git a/src/main/java/org/jcnc/snow/pkg/model/BuildConfiguration.java b/src/main/java/org/jcnc/snow/pkg/model/BuildConfiguration.java index c0f0075..de4b64c 100644 --- a/src/main/java/org/jcnc/snow/pkg/model/BuildConfiguration.java +++ b/src/main/java/org/jcnc/snow/pkg/model/BuildConfiguration.java @@ -6,16 +6,16 @@ import java.util.HashMap; /** * 构建配置对象,封装构建过程中的所有选项。 *

- * 支持基于模板变量(形如{@code @{key}})的选项值替换。 + * 支持模板变量(形如 @{key})的值自动替换。 *

*/ public final class BuildConfiguration { - /** 存储配置项的键值对 */ + /** 存储所有配置项 */ private final Map options; /** - * 私有构造函数,用于初始化配置项。 + * 私有构造函数,仅供工厂方法调用。 * * @param options 配置项键值对 */ @@ -24,14 +24,15 @@ public final class BuildConfiguration { } /** - * 基于原始配置项和变量属性创建配置对象。 - *

- * 会将原始配置中的所有值中的{@code @{key}}模板,替换为属性props中对应的值。 - *

+ * 基于原始配置项和属性集创建配置对象。 + *
    + *
  • 会将所有值中的 @{key} 模板变量,替换为 props 中对应的值
  • + *
  • 属性未匹配到时保留原模板
  • + *
* - * @param flat 原始的配置项,值中可包含模板变量(如@{name}) - * @param props 用于替换模板变量的属性集 - * @return 构建完成的配置对象 + * @param flat 原始配置项,值中可包含模板变量 + * @param props 变量替换用的属性集 + * @return 处理后生成的配置对象 */ public static BuildConfiguration fromFlatMap(Map flat, Map props) { Map resolved = new HashMap<>(); @@ -46,11 +47,11 @@ public final class BuildConfiguration { } /** - * 获取指定key对应的配置值。 + * 获取指定配置项的值。 * * @param key 配置项名称 - * @param def 默认值(若未找到key则返回此值) - * @return 配置项对应值,若不存在则返回默认值 + * @param def 默认值(未找到时返回) + * @return 配置项值,若不存在则返回默认值 */ public String get(String key, String def) { return options.getOrDefault(key, def); diff --git a/src/main/java/org/jcnc/snow/pkg/model/Dependency.java b/src/main/java/org/jcnc/snow/pkg/model/Dependency.java index 50f4bea..481682e 100644 --- a/src/main/java/org/jcnc/snow/pkg/model/Dependency.java +++ b/src/main/java/org/jcnc/snow/pkg/model/Dependency.java @@ -5,18 +5,23 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * 表示一个 Maven 风格的 group:artifact:version 依赖坐标。 + * group:artifact:version 依赖坐标对象。 *

- * 支持通过占位符和属性映射进行动态变量替换,可用于构建工具或依赖管理场景。 + * 支持占位符和属性映射进行动态变量替换,适用于 Snow 语言包管理和源码依赖场景。 *

* *
- * 示例:
+ * 示例用法:
  * Dependency dep = Dependency.fromString(
  *     "core", "com.example:core:@{version}",
  *     Map.of("version", "1.2.3")
  * );
  * 
+ * + * @param id 依赖唯一标识 + * @param group 组织/分组名 + * @param artifact 构件名 + * @param version 版本号 */ public record Dependency( String id, @@ -25,25 +30,23 @@ public record Dependency( String version ) { - /** - * 用于匹配 group:artifact:version 格式的正则表达式。 - */ + /** 匹配 group:artifact:version 格式的正则表达式。 */ private static final Pattern GAV = Pattern.compile("([^:]+):([^:]+):(.+)"); /** * 根据字符串坐标和属性映射创建依赖对象。 *

- * 坐标中的占位符如 {@code @{key}} 会用 props 中对应的值替换。 + * 坐标中的占位符(如 @{key})会用 props 中对应的值替换。 *

* * @param id 依赖唯一标识 - * @param coordinate 依赖坐标字符串,格式为 group:artifact:version,支持变量占位符 - * @param props 占位符替换的属性映射 + * @param coordinate 坐标字符串,格式为 group:artifact:version,支持占位符 + * @param props 占位符替换属性映射 * @return 解析后的 Dependency 实例 - * @throws IllegalArgumentException 如果坐标格式非法 + * @throws IllegalArgumentException 坐标格式非法时抛出 */ public static Dependency fromString(String id, String coordinate, Map props) { - // 替换 @{prop} 占位符 + // 替换占位符 String resolved = coordinate; for (Map.Entry p : props.entrySet()) { resolved = resolved.replace("@{" + p.getKey() + "}", p.getValue()); @@ -57,23 +60,23 @@ public record Dependency( } /** - * 生成依赖对应的标准仓库 jar 路径。 + * 生成依赖对应的源码文件路径。 *

- * 路径格式通常为:groupId/artifactId/version/artifactId-version.jar
- * 例如:com/example/core/1.2.3/core-1.2.3.jar + * 路径格式:groupId/artifactId/version/artifactId.snow + * 例如:com/example/core/1.2.3/core.snow *

* - * @return 仓库 jar 文件的相对路径 + * @return 仓库源码文件的相对路径 */ public String toPath() { String groupPath = group.replace('.', '/'); - return groupPath + "/" + artifact + "/" + version + "/" + artifact + "-" + version + ".jar"; + return groupPath + "/" + artifact + "/" + version + "/" + artifact + ".snow"; } /** * 返回该依赖的 group:artifact:version 字符串表示。 * - * @return Maven 坐标字符串 + * @return 坐标字符串 */ @Override public String toString() { diff --git a/src/main/java/org/jcnc/snow/pkg/model/Project.java b/src/main/java/org/jcnc/snow/pkg/model/Project.java index 2b09cdb..efedc97 100644 --- a/src/main/java/org/jcnc/snow/pkg/model/Project.java +++ b/src/main/java/org/jcnc/snow/pkg/model/Project.java @@ -8,42 +8,41 @@ import java.util.Map; /** * 表示一个软件包/模块的项目信息,包括元数据、属性、仓库、依赖和构建配置等。 *

- * 本类为不可变对象,仅提供getter,无setter。 - * 支持通过 {@link #fromFlatMap(Map)} 静态工厂方法从扁平Map快速创建。 + * 本类为不可变对象,仅提供 getter 方法,无 setter。
+ * 支持通过 {@link #fromFlatMap(Map)} 静态工厂方法,从扁平 Map 快速创建实例。 *

* *
- * Map<String,String> map = ...;
+ * Map<String, String> map = ...;
  * Project project = Project.fromFlatMap(map);
  * 
*/ public final class Project { - /** 组织/分组名(如com.example) */ + /** 组织/分组名(如 com.example) */ private final String group; - /** 构件/模块名(如app-core) */ + /** 构件/模块名(如 app-core) */ private final String artifact; /** 项目展示名称 */ private final String name; - /** 版本号(如1.0.0) */ + /** 版本号(如 1.0.0) */ private final String version; /** 项目描述 */ private final String description; /** 许可证标识 */ private final String license; - /** 项目主页URL */ + /** 项目主页 URL */ private final String homepage; /** 额外属性(不影响主字段,可用于模板/占位符) */ private final Map properties; - /** 仓库列表(仓库ID -> 仓库对象) */ + /** 仓库列表(仓库 ID -> 仓库对象) */ private final Map repositories; /** 依赖列表 */ private final List dependencies; /** 构建配置 */ private final BuildConfiguration build; - /** * 构造函数(私有),请使用 {@link #fromFlatMap(Map)} 创建实例。 */ @@ -74,7 +73,7 @@ public final class Project { } /** - * 通过扁平Map创建 Project 实例。约定key格式如下: + * 通过扁平 Map 创建 Project 实例。key 格式约定如下: *
    *
  • project.* —— 项目元数据
  • *
  • properties.* —— 额外属性
  • @@ -83,12 +82,11 @@ public final class Project { *
  • build.* —— 构建配置
  • *
* - * @param map 扁平的配置map + * @param map 扁平的配置 map * @return Project 实例 */ public static Project fromFlatMap(Map map) { - - // 1. simple project metadata + // 1. 基本元数据 String group = map.getOrDefault("project.group", "unknown"); String artifact = map.getOrDefault("project.artifact", "unknown"); String name = map.getOrDefault("project.name", artifact); @@ -123,14 +121,13 @@ public final class Project { } }); - // 5. build.* simply hand the subtree map + // 5. build.* Map buildMap = new LinkedHashMap<>(); map.forEach((k, v) -> { if (k.startsWith("build.")) { buildMap.put(k.substring("build.".length()), v); } }); - BuildConfiguration buildCfg = BuildConfiguration.fromFlatMap(buildMap, props); return new Project(group, artifact, name, version, description, license, homepage, props, repos, deps, buildCfg); @@ -161,12 +158,12 @@ public final class Project { return description; } - /** @return 许可证 */ + /** @return 许可证标识 */ public String getLicense() { return license; } - /** @return 项目主页URL */ + /** @return 项目主页 URL */ public String getHomepage() { return homepage; } diff --git a/src/main/java/org/jcnc/snow/pkg/model/Repository.java b/src/main/java/org/jcnc/snow/pkg/model/Repository.java index 3e839f9..73d97b2 100644 --- a/src/main/java/org/jcnc/snow/pkg/model/Repository.java +++ b/src/main/java/org/jcnc/snow/pkg/model/Repository.java @@ -3,16 +3,16 @@ package org.jcnc.snow.pkg.model; /** * 表示一个远程仓库的基本信息,通常用于依赖解析和发布。 *

- * 每个仓库由唯一的ID和对应的URL确定。 + * 每个仓库由唯一的 ID 和对应的 URL 标识。 *

* *
- * 示例:
+ * 示例用法:
  * Repository repo = new Repository("central", "https://");
  * 
* * @param id 仓库唯一标识 - * @param url 仓库地址(一般为HTTP(S)链接) + * @param url 仓库地址(通常为 HTTP(S) 链接) */ public record Repository(String id, String url) { } diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/CleanTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/CleanTask.java index 0b5c1c5..80700d6 100644 --- a/src/main/java/org/jcnc/snow/pkg/tasks/CleanTask.java +++ b/src/main/java/org/jcnc/snow/pkg/tasks/CleanTask.java @@ -6,17 +6,27 @@ import java.nio.file.Path; import java.util.Comparator; /** - * 清理构建输出目录(如 build 和 dist)的任务实现。 + * 用于清理构建输出目录(如 {@code build} 和 {@code dist})的任务实现类。 *

- * 实现 {@link Task} 接口,通常用于构建流程中的清理阶段。 + * 实现 {@link Task} 接口,常用于自动化构建流程的清理阶段,负责递归删除指定的构建产物目录。 *

+ *

+ * 本类为无状态实现,线程安全。 + *

+ * + *

示例用法:

+ *
{@code
+ * Task clean = new CleanTask();
+ * clean.run();
+ * }
*/ public final class CleanTask implements Task { /** - * 执行清理任务,删除 "build" 和 "dist" 目录及其所有内容。 + * 执行清理任务,递归删除当前目录下的 {@code build} 和 {@code dist} 目录及其所有内容。 + * 如果目标目录不存在,则跳过不处理。 * - * @throws IOException 删除目录过程中出现 IO 错误时抛出 + * @throws IOException 删除目录或文件过程中发生 IO 错误时抛出 */ @Override public void run() throws IOException { @@ -27,10 +37,15 @@ public final class CleanTask implements Task { /** * 递归删除指定目录及其所有子文件和子目录。 - * 使用 try-with-resources 自动关闭文件流,避免资源泄漏。 + *

+ * 若目录不存在,则直接返回。 + *

+ *

+ * 内部使用 try-with-resources 保证文件流自动关闭,避免资源泄漏。 + *

* * @param dir 需要删除的目录路径 - * @throws IOException 删除过程中出现 IO 错误时抛出 + * @throws IOException 删除目录或文件过程中发生 IO 错误时抛出 */ private void deleteDir(Path dir) throws IOException { if (Files.notExists(dir)) return; diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/CompileTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/CompileTask.java index fb3fb99..04519d7 100644 --- a/src/main/java/org/jcnc/snow/pkg/tasks/CompileTask.java +++ b/src/main/java/org/jcnc/snow/pkg/tasks/CompileTask.java @@ -1,43 +1,264 @@ package org.jcnc.snow.pkg.tasks; +import org.jcnc.snow.cli.commands.CompileCommand; import org.jcnc.snow.pkg.model.Project; +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.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.*; /** - * 编译项目源代码的任务实现。 + * CLI 任务:编译 .snow 源文件为 VM 字节码(.water 文件)。 *

- * 实现 {@link Task} 接口,用于构建流程中的编译阶段。当前仅为示例,未集成实际编译器。 + * 支持单文件、多文件和目录递归编译,并可在编译后立即运行虚拟机。
+ * 命令行参数支持 run、-o、-d 及直接指定源文件。 *

+ * + *
+ * 用法示例:
+ * $ snow compile [run] [-o <name>] [-d <srcDir>] [file1.snow file2.snow ...]
+ * 
*/ public final class CompileTask implements Task { - - /** 待编译的项目 */ + /** 项目信息 */ private final Project project; + /** 原始命令行参数 */ + private final String[] args; /** - * 创建 CompileTask 实例。 + * 创建一个编译任务。 * - * @param project 目标项目 + * @param project 项目信息对象 + * @param args 命令行参数数组 */ - public CompileTask(Project project) { + public CompileTask(Project project, String[] args) { this.project = project; + this.args = args; } /** - * 执行编译任务,打印源代码目录和输出目录。 - *

- * 实际编译尚未实现(TODO)。 - *

+ * 创建一个不带参数的编译任务。 * - * @throws Exception 预留,未来集成编译器可能抛出异常 + * @param project 项目信息对象 + */ + public CompileTask(Project project) { + this(project, new String[0]); + } + + /** + * 执行编译任务。该方法会解析参数并调用 {@link #execute(String[])} 进行实际编译流程。 + * + * @throws Exception 执行过程中出现任意异常时抛出 */ @Override public void run() throws Exception { - // 获取源码目录和输出目录,默认分别为 "src" 和 "build/classes" - Path srcDir = Path.of(project.getProperties().getOrDefault("src_dir", "src")); - Path outDir = Path.of(project.getProperties().getOrDefault("output_dir", "build/classes")); - System.out.println("[compile] sources=" + srcDir + " output=" + outDir); - // TODO: 集成实际的编译器 + execute(this.args); + } + + /** + * 编译 .snow 源文件为 VM 字节码,并可选择立即运行。 + *
    + *
  • 支持参数 run(编译后运行)、-o(输出文件名)、-d(递归目录)、直接指定多个源文件。
  • + *
  • 输出源代码、AST、IR、最终 VM code,并写出 .water 文件。
  • + *
+ * + * @param args 命令行参数数组 + * @return 0 表示成功,非 0 表示失败 + * @throws Exception 编译或写入过程中出现异常时抛出 + */ + public int execute(String[] args) throws Exception { + // ---------------- 解析命令行参数 ---------------- + boolean runAfterCompile = false; + String outputName = null; + Path dir = null; + List sources = new ArrayList<>(); + + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + switch (arg) { + case "run" -> runAfterCompile = true; + case "-o" -> { + if (i + 1 < args.length) outputName = args[++i]; + else { + System.err.println("Missing argument for -o"); + new CompileCommand().printUsage(); + return 1; + } + } + case "-d" -> { + if (i + 1 < args.length) dir = Path.of(args[++i]); + else { + System.err.println("Missing argument for -d"); + new CompileCommand().printUsage(); + return 1; + } + } + default -> { + if (arg.endsWith(".snow")) { + sources.add(Path.of(arg)); + } else { + System.err.println("Unknown option or file: " + arg); + new CompileCommand().printUsage(); + return 1; + } + } + } + } + + // --------- 如果指定了目录则递归收集所有 *.snow --------- + if (dir != null) { + if (!Files.isDirectory(dir)) { + System.err.println("Not a directory: " + dir); + return 1; + } + try (var stream = Files.walk(dir)) { + stream.filter(p -> p.toString().endsWith(".snow")) + .sorted() // 确保稳定顺序 + .forEach(sources::add); + } + } + + if (sources.isEmpty()) { + System.err.println("No .snow source files found."); + return 1; + } + + // 多文件但未指定 -o 且非目录编译 —— 提示必须指定输出名 + if (sources.size() > 1 && outputName == null && dir == null) { + System.err.println("Please specify output name using -o "); + return 1; + } + + // ---------------- 1. 词法/语法分析,并打印源代码 ---------------- + List allAst = new ArrayList<>(); + + System.out.println("## 编译器输出"); + System.out.println("### Snow 源代码"); + + 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); + + // 打印源码 + 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()); + } + + // ---------------- 2. 语义分析 ---------------- + SemanticAnalyzerRunner.runSemanticAnalysis(allAst, false); + + // ---------------- 3. AST → IR,并将 main 函数移动至首位 ---------------- + IRProgram program = new IRProgramBuilder().buildProgram(allAst); + program = reorderForEntry(program); + + // 打印 AST 和 IR + 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. 写出 .water 文件 ---------------- + Path outputFile = deriveOutputPath(sources, outputName, dir); + Files.write(outputFile, finalCode, StandardCharsets.UTF_8); + System.out.println("Written to " + outputFile.toAbsolutePath()); + + // ---------------- 6. 可选:立即运行 VM ---------------- + if (runAfterCompile) { + System.out.println("\n=== Launching VM ==="); + VMLauncher.main(new String[]{outputFile.toString()}); + } + + return 0; + } + + /** + * 推断 .water 输出文件名。 + *
    + *
  • 如果指定 -o,直接使用该名称。
  • + *
  • 目录编译时,取目录名。
  • + *
  • 单文件编译时,取文件名去掉 .snow 后缀。
  • + *
  • 否则默认 "program"。
  • + *
+ * + * @param sources 源文件路径列表 + * @param outName 输出文件名(如有指定,否则为 null) + * @param dir 源码目录(如有指定,否则为 null) + * @return 推断出的输出文件路径(.water 文件) + */ + private static Path deriveOutputPath(List sources, String outName, Path dir) { + String base; + if (outName != null) { + base = outName; + } else if (dir != null) { + base = dir.getFileName().toString(); + } else if (sources.size() == 1) { + base = sources.getFirst().getFileName().toString() + .replaceFirst("\\.snow$", ""); + } else { + base = "program"; + } + return Path.of(base + ".water"); + } + + /** + * 将 main 函数调整至函数列表首位,确保程序入口为 PC=0。 + * + * @param in 原始 IRProgram + * @return 调整入口后的 IRProgram + */ + private static IRProgram reorderForEntry(IRProgram in) { + List ordered = new ArrayList<>(in.functions()); + for (int i = 0; i < ordered.size(); i++) { + if ("main".equals(ordered.get(i).name())) { + Collections.swap(ordered, 0, i); + break; + } + } + IRProgram out = new IRProgram(); + ordered.forEach(out::add); + return out; } } diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/GenerateTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/GenerateTask.java new file mode 100644 index 0000000..83b9253 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/tasks/GenerateTask.java @@ -0,0 +1,95 @@ +package org.jcnc.snow.pkg.tasks; + +import org.jcnc.snow.pkg.model.Project; +import org.jcnc.snow.pkg.utils.SnowExample; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * 项目脚手架生成任务。
+ * 根据 {@link Project} 元数据自动创建标准项目目录结构,并生成示例入口文件 + * main.snow。 + * + *

+ * 生成内容包括: + *

    + *
  • src/ —— 源码根目录
  • + *
  • src/{group package}/ —— 按 project.group 创建的包路径 + * (如 com.examplesrc/com/example/
  • + *
  • test/ —— 测试源码目录
  • + *
  • build/ —— 编译输出目录
  • + *
  • dist/ —— 打包输出目录
  • + *
  • src/{group package}/main.snow —— 示例入口文件
  • + *
+ * 如目录或入口文件已存在,则自动跳过,不会覆盖。 + *

+ */ +public final class GenerateTask implements Task { + + /** 项目信息元数据 */ + private final Project project; + + /** + * 创建项目生成任务。 + * + * @param project 项目信息元数据对象 + */ + public GenerateTask(Project project) { + this.project = project; + } + + /** + * 执行脚手架生成流程,创建标准目录和入口示例文件。 + *
    + *
  • 若相关目录不存在则创建
  • + *
  • 若设置了 project.group,则在 src/ 下新建对应包路径
  • + *
  • 示例入口文件 main.snow 写入包路径目录
  • + *
  • 生成过程输出进度信息
  • + *
+ * + * @throws IOException 创建目录或写入文件时发生 IO 错误时抛出 + */ + @Override + public void run() throws IOException { + Path root = Paths.get(".").toAbsolutePath(); + + /* ---------- 1. 构造待创建目录列表 ---------- */ + List dirs = new ArrayList<>(List.of( + root.resolve("src"), + root.resolve("test"), + root.resolve("build"), + root.resolve("dist") + )); + + /* ---------- 2. 处理 group:追加包目录 ---------- */ + String group = project != null ? project.getGroup() : null; + Path srcDir = root.resolve("src"); + Path packageDir = srcDir; // 默认直接在 src 下 + if (group != null && !group.isBlank()) { + packageDir = srcDir.resolve(group.replace('.', '/')); + dirs.add(packageDir); + } + + /* ---------- 3. 创建目录 ---------- */ + for (Path dir : dirs) { + if (Files.notExists(dir)) { + Files.createDirectories(dir); + System.out.println("[generate] created directory " + root.relativize(dir)); + } + } + + /* ---------- 4. 写入示例入口文件 main.snow ---------- */ + Path mainSnow = packageDir.resolve("main.snow"); + if (Files.notExists(mainSnow)) { + Files.writeString(mainSnow, SnowExample.getMainModule()); + System.out.println("[generate] created " + root.relativize(mainSnow)); + } + + System.out.println("[generate] project scaffold is ready."); + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/PackageTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/PackageTask.java index 7524f73..a04dcf0 100644 --- a/src/main/java/org/jcnc/snow/pkg/tasks/PackageTask.java +++ b/src/main/java/org/jcnc/snow/pkg/tasks/PackageTask.java @@ -9,29 +9,38 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** - * 项目打包任务,将编译输出目录(如 build/classes)打包为 .ice 文件。 + * 项目打包任务,将编译输出目录(如 build/classes)打包为 .ice 文件。 *

- * 实现 {@link Task} 接口,通常用于构建流程中的打包阶段。 + * 实现 {@link Task} 接口,通常用于构建流程的打包阶段。
+ * 只会打包 build/classes 目录下所有文件,不含其他目录。 + *

+ *

+ * 输出文件位于 dist 目录,命名为 artifact-version.ice。 *

*/ public final class PackageTask implements Task { - /** 目标项目 */ + /** 目标项目元数据 */ private final Project project; /** - * 创建 PackageTask 实例。 + * 创建打包任务。 * - * @param project 目标项目 + * @param project 目标项目元数据 */ public PackageTask(Project project) { this.project = project; } /** - * 执行打包任务,将编译输出目录压缩为 artifact-version.ice 文件。 + * 执行打包任务,将 build/classes 目录下所有文件压缩为 dist/artifact-version.ice。 + *
    + *
  • 若输出目录 dist 不存在会自动创建
  • + *
  • 只打包 build/classes 下所有普通文件(保持相对目录结构)
  • + *
  • 如无 build/classes 则不会生成包
  • + *
* - * @throws Exception 打包过程中出现 IO 或其他异常时抛出 + * @throws Exception 打包过程中发生 IO 或其他异常时抛出 */ @Override public void run() throws Exception { @@ -43,15 +52,15 @@ public final class PackageTask implements Task { Path packageFile = distDir.resolve(fileName); try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(packageFile))) { - // 仅将编译输出目录打包 + // 仅将 build/classes 目录打包 Path classesDir = Path.of("build/classes"); if (Files.exists(classesDir)) { - // 使用 try-with-resources 正确关闭 Stream + // 遍历所有文件并写入 zip try (Stream stream = Files.walk(classesDir)) { stream.filter(Files::isRegularFile) .forEach(p -> { try { - // 将文件以相对路径加入压缩包 + // 以 classesDir 为根的相对路径存入 zip zos.putNextEntry(new ZipEntry(classesDir.relativize(p).toString())); Files.copy(p, zos); zos.closeEntry(); diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/PublishTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/PublishTask.java index d67b0e1..310b8fa 100644 --- a/src/main/java/org/jcnc/snow/pkg/tasks/PublishTask.java +++ b/src/main/java/org/jcnc/snow/pkg/tasks/PublishTask.java @@ -5,25 +5,26 @@ import org.jcnc.snow.pkg.model.Project; /** * 发布项目构件到远程仓库的任务实现。 *

- * 实现 {@link Task} 接口,通常用于构建流程中的发布阶段。目前仅为演示,尚未实现实际上传。 + * 实现 {@link Task} 接口,通常用于构建流程中的发布阶段。 + * 当前仅输出发布提示,尚未实现实际上传功能。 *

*/ public final class PublishTask implements Task { - /** 目标项目 */ + /** 目标项目元数据 */ private final Project project; /** - * 创建 PublishTask 实例。 + * 创建发布任务。 * - * @param project 目标项目 + * @param project 目标项目元数据 */ public PublishTask(Project project) { this.project = project; } /** - * 执行发布任务。当前仅打印发布提示,未实现实际上传逻辑。 + * 执行发布任务。目前仅打印发布提示信息,未实现实际上传逻辑。 * * @throws Exception 预留,未来实现上传逻辑时可能抛出异常 */ diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/RunTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/RunTask.java new file mode 100644 index 0000000..ce83966 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/tasks/RunTask.java @@ -0,0 +1,44 @@ +package org.jcnc.snow.pkg.tasks; + +import org.jcnc.snow.vm.VMLauncher; + +/** + * 任务:执行已编译的 VM 字节码文件(.water)。 + *

+ * 作为 CLI、IDE 插件或其他宿主环境启动虚拟机的统一入口,
+ * 通过调用 {@link VMLauncher#main(String[])} 启动 VM 并执行指定程序。 + *

+ */ +public final class RunTask implements Task { + + /** + * 传递给虚拟机的完整参数列表(第一个应为 .water 文件路径) + */ + private final String[] args; + + /** + * 创建运行任务。 + * + * @param args VM 参数数组(第一个为 .water 程序路径,其后为可选参数) + */ + public RunTask(String... args) { + this.args = args; + } + + /** + * 执行运行任务。内部委托 {@link VMLauncher#main(String[])} 启动 VM。 + *
    + *
  • 如果参数为空则抛出 {@link IllegalArgumentException}
  • + *
  • 异常由虚拟机本身抛出,直接透出
  • + *
+ * + * @throws Exception 虚拟机启动或运行期间抛出的异常 + */ + @Override + public void run() throws Exception { + if (args == null || args.length == 0) { + throw new IllegalArgumentException("VM run requires at least the program file path."); + } + VMLauncher.main(args); + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/Task.java b/src/main/java/org/jcnc/snow/pkg/tasks/Task.java index 755adc2..071fdbc 100644 --- a/src/main/java/org/jcnc/snow/pkg/tasks/Task.java +++ b/src/main/java/org/jcnc/snow/pkg/tasks/Task.java @@ -1,9 +1,9 @@ package org.jcnc.snow.pkg.tasks; /** - * 构建任务的通用接口,所有具体任务都应实现该接口。 + * 构建任务的通用接口,所有具体任务(如编译、打包、清理等)都应实现该接口。 *

- * 用于统一生命周期内的任务行为,例如编译、打包、清理等。 + * 用于统一不同阶段任务的生命周期与执行行为。 *

*/ public interface Task { @@ -11,7 +11,7 @@ public interface Task { /** * 执行具体任务的入口方法。 * - * @throws Exception 任务执行过程中出现的异常 + * @throws Exception 任务执行过程中出现的任意异常 */ void run() throws Exception; } diff --git a/src/main/java/org/jcnc/snow/pkg/utils/SnowExample.java b/src/main/java/org/jcnc/snow/pkg/utils/SnowExample.java new file mode 100644 index 0000000..6aea927 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/utils/SnowExample.java @@ -0,0 +1,58 @@ +package org.jcnc.snow.pkg.utils; + +/** + * 示例模块模板工具类,提供 main.snow 的标准示例代码字符串。 + *

+ * 用于项目脚手架生成或帮助用户快速上手 .snow 语言。 + *

+ */ +public final class SnowExample { + + /** + * 工具类构造方法,禁止实例化。 + */ + private SnowExample() { + // 工具类不允许实例化 + } + + /** + * 获取 main.snow 示例模块的内容字符串。 + * + * @return main.snow 示例模块的完整代码 + */ + public static String getMainModule() { + return """ + module: Math + function: main + parameter: + return_type: int + body: + Math.factorial(6) + return 0 + end body + end function + + function: factorial + parameter: + declare n:int + return_type: int + body: + declare num1:int = 1 + loop: + initializer: + declare counter:int = 1 + condition: + counter <= n + update: + counter = counter + 1 + body: + num1 = num1 * counter + end body + end loop + return num1 + end body + end function + end module + """; + } +}