implements PLN-0010 builder pipeline entrypoints

This commit is contained in:
bQUARKz 2026-03-30 19:01:07 +01:00
parent df8a98488a
commit c467a2642f
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
10 changed files with 217 additions and 32 deletions

View File

@ -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":[]}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<BuildingIssue> diagnostics,
ResolvedWorkspace resolvedWorkspace,
FileTable fileTable,
IRBackend irBackend) {
public AnalysisSnapshot {
diagnostics = List.copyOf(diagnostics);
}
}

View File

@ -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<BuildingIssue> diagnostics,
Path bytecodeArtifactPath) {
public BuildResult {
diagnostics = List.copyOf(diagnostics);
}
}

View File

@ -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);
}
}

View File

@ -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<BuildingIssue> diagnostics,
IRVMProgram irvm,
IRVMProgram optimizedIrvm,
BytecodeModule bytecodeModule,
byte[] bytecodeBytes) {
public CompileResult {
diagnostics = List.copyOf(diagnostics);
bytecodeBytes = bytecodeBytes == null ? null : Arrays.copyOf(bytecodeBytes, bytecodeBytes.length);
}
}

View File

@ -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<BuildingIssue> runToTerminal(
final BuilderPipelineContext ctx,
final LogAggregator logs,
final Class<? extends PipelineStage> terminalStage) {
final var diagnostics = new ArrayList<BuildingIssue>();
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<BuildingIssue> issues,
final LogAggregator logs) {
if (ReadOnlyCollection.isEmpty(issues)) {
return;
}
for (final var issue : issues) {
issue.print(logs);
}
}
}

View File

@ -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) {