diff --git a/discussion/index.ndjson b/discussion/index.ndjson index a23868bf..6bc42104 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -9,4 +9,4 @@ {"type":"discussion","id":"DSC-0008","status":"done","ticket":"pbs-low-level-asset-manager-surface","title":"PBS Low-Level Asset Manager Surface for Runtime AssetManager","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","runtime","asset-manager","host-abi","stdlib","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0008-pbs-low-level-asset-manager-surface/LSN-0023-lowassets-runtime-aligned-sdk-surface.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} {"type":"discussion","id":"DSC-0009","status":"open","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["studio","debugger","workspace","integration","shell"],"agendas":[{"id":"AGD-0009","file":"AGD-0009-studio-debugger-workspace-integration.md","status":"open","created_at":"2026-03-30","updated_at":"2026-03-30"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0010","status":"open","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[{"id":"AGD-0010","file":"AGD-0010-studio-code-editor-workspace-foundations.md","status":"open","created_at":"2026-03-30","updated_at":"2026-03-30"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0011","status":"open","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[{"id":"AGD-0011","file":"AGD-0011-compiler-analyze-compile-build-pipeline-split.md","status":"accepted","created_at":"2026-03-30","updated_at":"2026-03-30"}],"decisions":[{"id":"DEC-0007","file":"DEC-0007-compiler-analyze-compile-build-pipeline-split.md","status":"in_progress","created_at":"2026-03-30","updated_at":"2026-03-30","ref_agenda":"AGD-0011"}],"plans":[{"id":"PLN-0009","file":"PLN-0009-compiler-pipeline-spec-and-contract-propagation.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30","ref_decisions":["DEC-0007"]},{"id":"PLN-0010","file":"PLN-0010-refactor-builder-pipeline-service-into-entrypoints.md","status":"review","created_at":"2026-03-30","updated_at":"2026-03-30","ref_decisions":["DEC-0007"]},{"id":"PLN-0011","file":"PLN-0011-migrate-callsites-and-tests-to-build-compile-analyze.md","status":"review","created_at":"2026-03-30","updated_at":"2026-03-30","ref_decisions":["DEC-0007"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0011","status":"open","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[{"id":"AGD-0011","file":"AGD-0011-compiler-analyze-compile-build-pipeline-split.md","status":"accepted","created_at":"2026-03-30","updated_at":"2026-03-30"}],"decisions":[{"id":"DEC-0007","file":"DEC-0007-compiler-analyze-compile-build-pipeline-split.md","status":"in_progress","created_at":"2026-03-30","updated_at":"2026-03-30","ref_agenda":"AGD-0011"}],"plans":[{"id":"PLN-0009","file":"PLN-0009-compiler-pipeline-spec-and-contract-propagation.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30","ref_decisions":["DEC-0007"]},{"id":"PLN-0010","file":"PLN-0010-refactor-builder-pipeline-service-into-entrypoints.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30","ref_decisions":["DEC-0007"]},{"id":"PLN-0011","file":"PLN-0011-migrate-callsites-and-tests-to-build-compile-analyze.md","status":"review","created_at":"2026-03-30","updated_at":"2026-03-30","ref_decisions":["DEC-0007"]}],"lessons":[]} diff --git a/discussion/workflow/plans/PLN-0010-refactor-builder-pipeline-service-into-entrypoints.md b/discussion/workflow/plans/PLN-0010-refactor-builder-pipeline-service-into-entrypoints.md index 01b71475..34fff940 100644 --- a/discussion/workflow/plans/PLN-0010-refactor-builder-pipeline-service-into-entrypoints.md +++ b/discussion/workflow/plans/PLN-0010-refactor-builder-pipeline-service-into-entrypoints.md @@ -2,9 +2,9 @@ id: PLN-0010 ticket: compiler-analyze-compile-build-pipeline-split title: Refactor BuilderPipelineService into explicit analyze, compile, and build entrypoints -status: review +status: done created: 2026-03-30 -completed: +completed: 2026-03-30 tags: - compiler - pipeline diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/Compile.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/Compile.java index 2159c24e..4a97b288 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/Compile.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/Compile.java @@ -1,6 +1,7 @@ package p.studio.compiler; import p.studio.compiler.messages.BuilderPipelineConfig; +import p.studio.compiler.models.BuilderPipelineContext; import p.studio.compiler.workspaces.BuilderPipelineService; import p.studio.utilities.PConstants; import p.studio.utilities.logs.LogAggregator; @@ -9,6 +10,7 @@ public class Compile { public static void main(String[] args) { final var logAggregator = LogAggregator.stdout(); final var config = new BuilderPipelineConfig(false, "test-projects/%s".formatted(PConstants.PROJECT)); - BuilderPipelineService.INSTANCE.run(config, logAggregator); + final var context = BuilderPipelineContext.fromConfig(config); + BuilderPipelineService.INSTANCE.build(context, logAggregator); } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/messages/BuilderPipelineConfig.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/messages/BuilderPipelineConfig.java index ff3a045b..50ffb4af 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/messages/BuilderPipelineConfig.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/messages/BuilderPipelineConfig.java @@ -1,17 +1,28 @@ package p.studio.compiler.messages; +import p.studio.compiler.utilities.SourceProviderFactory; + public record BuilderPipelineConfig( boolean explain, String rootProjectPath, - String vmProfile) { + String vmProfile, + SourceProviderFactory sourceProviderFactory) { public BuilderPipelineConfig( final boolean explain, final String rootProjectPath) { - this(explain, rootProjectPath, "core-v1"); + this(explain, rootProjectPath, "core-v1", SourceProviderFactory.filesystem()); + } + + public BuilderPipelineConfig( + final boolean explain, + final String rootProjectPath, + final String vmProfile) { + this(explain, rootProjectPath, vmProfile, SourceProviderFactory.filesystem()); } public BuilderPipelineConfig { vmProfile = vmProfile == null || vmProfile.isBlank() ? "core-v1" : vmProfile; + sourceProviderFactory = sourceProviderFactory == null ? SourceProviderFactory.filesystem() : sourceProviderFactory; } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/AnalysisSnapshot.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/AnalysisSnapshot.java new file mode 100644 index 00000000..324f1a5b --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/AnalysisSnapshot.java @@ -0,0 +1,16 @@ +package p.studio.compiler.models; + +import p.studio.compiler.messages.BuildingIssue; +import p.studio.compiler.source.tables.FileTable; + +import java.util.List; + +public record AnalysisSnapshot( + List diagnostics, + ResolvedWorkspace resolvedWorkspace, + FileTable fileTable, + IRBackend irBackend) { + public AnalysisSnapshot { + diagnostics = List.copyOf(diagnostics); + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuildResult.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuildResult.java new file mode 100644 index 00000000..5c49bfba --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuildResult.java @@ -0,0 +1,15 @@ +package p.studio.compiler.models; + +import p.studio.compiler.messages.BuildingIssue; + +import java.nio.file.Path; +import java.util.List; + +public record BuildResult( + CompileResult compileResult, + List diagnostics, + Path bytecodeArtifactPath) { + public BuildResult { + diagnostics = List.copyOf(diagnostics); + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuilderPipelineContext.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuilderPipelineContext.java index 3d9add82..6dfaec6d 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuilderPipelineContext.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuilderPipelineContext.java @@ -30,7 +30,11 @@ public class BuilderPipelineContext { this.sourceProviderFactory = factory; } + public static BuilderPipelineContext fromConfig(final BuilderPipelineConfig config) { + return new BuilderPipelineContext(config, config.sourceProviderFactory()); + } + public static BuilderPipelineContext compilerContext(final BuilderPipelineConfig config) { - return new BuilderPipelineContext(config, SourceProviderFactory.filesystem()); + return fromConfig(config); } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/CompileResult.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/CompileResult.java new file mode 100644 index 00000000..312b84b8 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/CompileResult.java @@ -0,0 +1,21 @@ +package p.studio.compiler.models; + +import p.studio.compiler.backend.bytecode.BytecodeModule; +import p.studio.compiler.backend.irvm.IRVMProgram; +import p.studio.compiler.messages.BuildingIssue; + +import java.util.Arrays; +import java.util.List; + +public record CompileResult( + AnalysisSnapshot analysisSnapshot, + List diagnostics, + IRVMProgram irvm, + IRVMProgram optimizedIrvm, + BytecodeModule bytecodeModule, + byte[] bytecodeBytes) { + public CompileResult { + diagnostics = List.copyOf(diagnostics); + bytecodeBytes = bytecodeBytes == null ? null : Arrays.copyOf(bytecodeBytes, bytecodeBytes.length); + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java index 738d09cc..273f7d25 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java @@ -2,12 +2,16 @@ package p.studio.compiler.workspaces; import lombok.extern.slf4j.Slf4j; import p.studio.compiler.exceptions.BuildException; -import p.studio.compiler.messages.BuilderPipelineConfig; +import p.studio.compiler.messages.BuildingIssue; import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.compiler.models.AnalysisSnapshot; +import p.studio.compiler.models.BuildResult; +import p.studio.compiler.models.CompileResult; import p.studio.compiler.workspaces.stages.*; import p.studio.utilities.logs.LogAggregator; import p.studio.utilities.structures.ReadOnlyCollection; +import java.util.ArrayList; import java.util.List; @Slf4j @@ -35,29 +39,90 @@ public class BuilderPipelineService { this.stages = stages; } - public void run( - final BuilderPipelineConfig config, + public AnalysisSnapshot analyze( + final BuilderPipelineContext ctx, final LogAggregator logs) { - final var ctx = BuilderPipelineContext.compilerContext(config); + final var diagnostics = runToTerminal(ctx, logs, FrontendPhasePipelineStage.class); + return new AnalysisSnapshot( + diagnostics, + ctx.resolvedWorkspace, + ctx.fileTable, + ctx.irBackend); + } + public CompileResult compile( + final BuilderPipelineContext ctx, + final LogAggregator logs) { + final var diagnostics = runToTerminal(ctx, logs, VerifyBytecodePipelineStage.class); + final var analysisSnapshot = new AnalysisSnapshot( + diagnostics, + ctx.resolvedWorkspace, + ctx.fileTable, + ctx.irBackend); + return new CompileResult( + analysisSnapshot, + diagnostics, + ctx.irvm, + ctx.optimizedIrvm, + ctx.bytecodeModule, + ctx.bytecodeBytes); + } + + public BuildResult build( + final BuilderPipelineContext ctx, + final LogAggregator logs) { + final var diagnostics = runToTerminal(ctx, logs, WriteBytecodeArtifactPipelineStage.class); + final var analysisSnapshot = new AnalysisSnapshot( + diagnostics, + ctx.resolvedWorkspace, + ctx.fileTable, + ctx.irBackend); + final var compileResult = new CompileResult( + analysisSnapshot, + diagnostics, + ctx.irvm, + ctx.optimizedIrvm, + ctx.bytecodeModule, + ctx.bytecodeBytes); + return new BuildResult( + compileResult, + diagnostics, + ctx.bytecodeArtifactPath); + } + + private List runToTerminal( + final BuilderPipelineContext ctx, + final LogAggregator logs, + final Class terminalStage) { + final var diagnostics = new ArrayList(); + var completed = false; for (final var builderPipelineStage : stages) { final var issues = builderPipelineStage.run(ctx, logs); - var error = false; - if (ReadOnlyCollection.isNotEmpty(issues)) { - for (final var issue : issues) { - if (issue.isError()) { - error = true; - logs.using(log).error(issue.getMessage(), issue.getException()); - continue; - } - logs.using(log).warn(issue.getMessage()); - } - } - if (error) { + diagnostics.addAll(issues.asCollection()); + printIssues(issues, logs); + if (issues.hasErrors()) { throw new BuildException("issues found on pipeline stage: " + builderPipelineStage.getClass().getSimpleName()); } + if (terminalStage.isInstance(builderPipelineStage)) { + completed = true; + break; + } } + if (!completed) { + throw new BuildException("terminal stage not found on builder pipeline: " + terminalStage.getSimpleName()); + } + logs.using(log).info("builder pipeline completed successfully through " + terminalStage.getSimpleName()); + return List.copyOf(diagnostics); + } - logs.using(log).info("builder pipeline completed successfully"); + private void printIssues( + final ReadOnlyCollection issues, + final LogAggregator logs) { + if (ReadOnlyCollection.isEmpty(issues)) { + return; + } + for (final var issue : issues) { + issue.print(logs); + } } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/MainProjectPipelineIntegrationTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/MainProjectPipelineIntegrationTest.java index 39fff96f..eb393b09 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/MainProjectPipelineIntegrationTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/MainProjectPipelineIntegrationTest.java @@ -2,6 +2,7 @@ package p.studio.compiler.integration; import org.junit.jupiter.api.Test; import p.studio.compiler.messages.BuilderPipelineConfig; +import p.studio.compiler.models.BuilderPipelineContext; import p.studio.compiler.workspaces.BuilderPipelineService; import p.studio.utilities.logs.LogAggregator; @@ -14,26 +15,76 @@ import static org.junit.jupiter.api.Assertions.*; class MainProjectPipelineIntegrationTest { @Test - void shouldCompileMainProjectAndWriteProgramBytecode() throws IOException { + void analyzeShouldNotWriteProgramBytecode() throws IOException { + final var projectRoot = projectRoot(); + final var outputPath = resetOutput(projectRoot); + final var logs = bufferedLogs(); + final var context = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, projectRoot.toString())); + + final var snapshot = assertDoesNotThrow( + () -> BuilderPipelineService.INSTANCE.analyze(context, logs), + () -> "analyze unexpectedly failed for " + projectRoot); + + assertNotNull(snapshot.resolvedWorkspace(), "analyze must expose resolved workspace"); + assertNotNull(snapshot.fileTable(), "analyze must expose file table"); + assertNotNull(snapshot.irBackend(), "analyze must expose frontend semantic result"); + assertFalse(Files.exists(outputPath), "analyze must not write output: " + outputPath); + } + + @Test + void compileShouldProduceInMemoryBytecodeWithoutWritingProgramBytecode() throws IOException { + final var projectRoot = projectRoot(); + final var outputPath = resetOutput(projectRoot); + final var logs = bufferedLogs(); + final var context = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, projectRoot.toString())); + + final var result = assertDoesNotThrow( + () -> BuilderPipelineService.INSTANCE.compile(context, logs), + () -> "compile unexpectedly failed for " + projectRoot); + + assertNotNull(result.analysisSnapshot(), "compile must retain analysis snapshot"); + assertNotNull(result.bytecodeModule(), "compile must expose bytecode module"); + assertNotNull(result.bytecodeBytes(), "compile must expose bytecode bytes"); + assertTrue(result.bytecodeBytes().length > 0, "compile must expose non-empty bytecode bytes"); + assertFalse(Files.exists(outputPath), "compile must not write output: " + outputPath); + } + + @Test + void buildShouldWriteProgramBytecode() throws IOException { final var repoRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()); final var projectRoot = repoRoot.resolve("test-projects").resolve("main"); + final var outputPath = resetOutput(projectRoot); + final var logs = bufferedLogs(); + final var context = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, projectRoot.toString())); + + final var result = assertDoesNotThrow( + () -> BuilderPipelineService.INSTANCE.build(context, logs), + () -> "build unexpectedly failed for " + projectRoot); + assertEquals(outputPath, result.bytecodeArtifactPath(), "build must expose written artifact path"); + assertTrue(Files.exists(outputPath), "build did not write output: " + outputPath); + assertTrue(Files.size(outputPath) > 0, "build wrote empty bytecode file: " + outputPath); + } + + private Path projectRoot() { + final var repoRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()); + return repoRoot.resolve("test-projects").resolve("main"); + } + + private Path resetOutput(final Path projectRoot) throws IOException { final var outputPath = projectRoot.resolve("build").resolve("program.pbx"); Files.createDirectories(outputPath.getParent()); Files.deleteIfExists(outputPath); + return outputPath; + } + private LogAggregator bufferedLogs() { final var logsOut = new StringBuilder(); - final var logs = LogAggregator.with(line -> { + return LogAggregator.with(line -> { logsOut.append(line); if (!line.endsWith("\n")) { logsOut.append('\n'); } }); - - assertDoesNotThrow( - () -> BuilderPipelineService.INSTANCE.run(new BuilderPipelineConfig(false, projectRoot.toString()), logs), - logsOut::toString); - assertTrue(Files.exists(outputPath), "pipeline did not write output: " + outputPath + "\n" + logsOut); - assertTrue(Files.size(outputPath) > 0, "pipeline wrote empty bytecode file: " + outputPath + "\n" + logsOut); } private Path findRepoRoot(final Path start) {