diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index 8f9dc61e..9714bd60 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -38,6 +38,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -191,13 +192,26 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { } final var blockedModuleIds = blockedModulesByDependency(failedModuleIds, moduleDependencyGraph); + final var entryPointCallableName = PBSDefinitions.PBS.getEntryPointCallableName(); + final var entryPointModuleCandidates = new LinkedHashSet(); for (final var compiledSource : compiledSourceFiles) { if (blockedModuleIds.contains(compiledSource.moduleId())) { continue; } irBackendAggregator.merge(compiledSource.irBackendFile()); + for (final var executable : compiledSource.irBackendFile().executableFunctions()) { + if (!entryPointCallableName.equals(executable.callableName())) { + continue; + } + if (executable.moduleId() != null && !executable.moduleId().isNone()) { + entryPointModuleCandidates.add(executable.moduleId()); + } + } + } + irBackendAggregator.entryPointCallableName(entryPointCallableName); + if (entryPointModuleCandidates.size() == 1) { + irBackendAggregator.entryPointModuleId(entryPointModuleCandidates.iterator().next()); } - irBackendAggregator.entryPointCallableName(PBSDefinitions.PBS.getEntryPointCallableName()); final var irBackend = irBackendAggregator.emit(); logs.using(log).debug("PBS frontend lowered to IR BE:\n%s".formatted(irBackend)); diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java index 94ea078c..05710baf 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java @@ -296,6 +296,7 @@ public class LowerToIRVMService { IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_DECLARATION_MISSING, "frontend IRBackend entrypoint declaration is missing"); } + final var entryPointModuleId = backend.getEntryPointModuleId(); final var sorted = new ArrayList<>(backend.getExecutableFunctions().asList()); sorted.sort((left, right) -> { final var leftModuleKey = moduleSortKey(backend, left.moduleId(), left.moduleKey()); @@ -314,15 +315,39 @@ public class LowerToIRVMService { } return Integer.compare(left.sourceStart(), right.sourceStart()); }); - final var entrypoints = sorted.stream() - .filter(candidate -> entryPointCallableName.equals(candidate.callableName())) - .toList(); + final java.util.List entrypoints; + if (entryPointModuleId != null && !entryPointModuleId.isNone()) { + final var expectedModuleIndex = entryPointModuleId.getIndex(); + entrypoints = sorted.stream() + .filter(candidate -> entryPointCallableName.equals(candidate.callableName())) + .filter(candidate -> !candidate.moduleId().isNone() && candidate.moduleId().getIndex() == expectedModuleIndex) + .toList(); + } else { + entrypoints = sorted.stream() + .filter(candidate -> entryPointCallableName.equals(candidate.callableName())) + .toList(); + } if (entrypoints.isEmpty()) { + if (entryPointModuleId != null && !entryPointModuleId.isNone()) { + throw new IRVMLoweringException( + IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_MISSING, + "missing qualified entrypoint callable '%s' for moduleId=%d".formatted( + entryPointCallableName, + entryPointModuleId.getIndex())); + } throw new IRVMLoweringException( IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_MISSING, "missing entrypoint callable '%s'".formatted(entryPointCallableName)); } if (entrypoints.size() > 1) { + if (entryPointModuleId != null && !entryPointModuleId.isNone()) { + throw new IRVMLoweringException( + IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_AMBIGUOUS, + "ambiguous qualified entrypoint: found %d callables named '%s' in moduleId=%d".formatted( + entrypoints.size(), + entryPointCallableName, + entryPointModuleId.getIndex())); + } throw new IRVMLoweringException( IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_AMBIGUOUS, "ambiguous entrypoint: found %d callables named '%s'".formatted( diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java index 3ca21b6c..95084e37 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java @@ -222,6 +222,41 @@ class LowerToIRVMServiceTest { assertEquals("helper_in_zeta", lowered.module().functions().get(2).name()); } + @Test + void lowerMustUseQualifiedEntrypointIdentityWhenProvided() { + final var backend = IRBackend.builder() + .entryPointCallableName("main") + .entryPointModuleId(new ModuleId(1)) + .modulePool(ReadOnlyList.from( + new ModuleReference("app", ReadOnlyList.from("alpha")), + new ModuleReference("app", ReadOnlyList.from("beta")))) + .executableFunctions(ReadOnlyList.from( + fnWithModuleAndLocals("main", "legacy/alpha", 0, 30, 1, ReadOnlyList.from(ret())), + fnWithModuleAndLocals("main", "legacy/beta", 1, 31, 9, ReadOnlyList.from(ret())), + fnWithModule("helper", "legacy/alpha", 0, 32, ReadOnlyList.from(ret())))) + .build(); + + final var lowered = new LowerToIRVMService().lower(backend); + + assertEquals("main", lowered.module().functions().get(0).name()); + assertEquals(9, lowered.module().functions().get(0).localSlots()); + } + + @Test + void lowerMustRejectMissingQualifiedEntrypointIdentity() { + final var backend = IRBackend.builder() + .entryPointCallableName("main") + .entryPointModuleId(new ModuleId(5)) + .modulePool(ReadOnlyList.from(new ModuleReference("app", ReadOnlyList.from("alpha")))) + .executableFunctions(ReadOnlyList.from( + fnWithModule("main", "legacy/alpha", 0, 40, ReadOnlyList.from(ret())))) + .build(); + + final var thrown = assertThrows(IRVMLoweringException.class, () -> new LowerToIRVMService().lower(backend)); + assertEquals(IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_MISSING, thrown.code()); + assertTrue(thrown.getMessage().contains("qualified entrypoint")); + } + private static IRBackendExecutableFunction fn( final String name, final String moduleKey, @@ -248,6 +283,16 @@ class LowerToIRVMServiceTest { final int moduleId, final int callableId, final ReadOnlyList instructions) { + return fnWithModuleAndLocals(name, moduleKey, moduleId, callableId, 0, instructions); + } + + private static IRBackendExecutableFunction fnWithModuleAndLocals( + final String name, + final String moduleKey, + final int moduleId, + final int callableId, + final int localSlots, + final ReadOnlyList instructions) { return new IRBackendExecutableFunction( new FileId(0), new ModuleId(moduleId), @@ -257,7 +302,7 @@ class LowerToIRVMServiceTest { 0, 10, 0, - 0, + localSlots, 0, 4, instructions, diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java index 5ea92a83..e06f531c 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java @@ -23,6 +23,8 @@ public class IRBackend { @Builder.Default private final String entryPointCallableName = "main"; @Builder.Default + private final ModuleId entryPointModuleId = ModuleId.none(); + @Builder.Default private final ReadOnlyList functions = ReadOnlyList.empty(); @Builder.Default private final ReadOnlyList executableFunctions = ReadOnlyList.empty(); @@ -41,6 +43,7 @@ public class IRBackend { public static final class IRBackendAggregator { private String entryPointCallableName; + private ModuleId entryPointModuleId = ModuleId.none(); private final ArrayList functions = new ArrayList<>(); private final ArrayList executableFunctions = new ArrayList<>(); private final ModuleTable moduleTable = new ModuleTable(); @@ -76,6 +79,13 @@ public class IRBackend { return this; } + public IRBackendAggregator entryPointModuleId(final ModuleId moduleId) { + if (moduleId != null) { + entryPointModuleId = moduleId; + } + return this; + } + private ModuleId[] reindexModules(final ReadOnlyList localModulePool) { if (localModulePool == null || localModulePool.isEmpty()) { return new ModuleId[0]; @@ -273,6 +283,7 @@ public class IRBackend { return IRBackend .builder() .entryPointCallableName(resolveEntryPointCallableName()) + .entryPointModuleId(resolveEntryPointModuleId()) .functions(ReadOnlyList.wrap(functions)) .executableFunctions(ReadOnlyList.wrap(executableFunctions)) .modulePool(emitModulePool()) @@ -291,12 +302,18 @@ public class IRBackend { ? "main" : entryPointCallableName; } + + private ModuleId resolveEntryPointModuleId() { + return entryPointModuleId == null ? ModuleId.none() : entryPointModuleId; + } } @Override public String toString() { final var sb = new StringBuilder(); sb.append("IRBackend{entrypoint=").append(entryPointCallableName) + .append('@') + .append(entryPointModuleId == null || entryPointModuleId.isNone() ? "-" : entryPointModuleId.getIndex()) .append(", functions=").append(functions.size()) .append(", executableFunctions=").append(executableFunctions.size()) .append(", modulePool=").append(modulePool.size()) diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java index 3516a81a..3dc29355 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java @@ -192,6 +192,18 @@ class IRBackendExecutableContractTest { final var backend = aggregator.emit(); assertEquals("frame", backend.getEntryPointCallableName()); + assertTrue(backend.getEntryPointModuleId().isNone()); + } + + @Test + void aggregatorMustEmitConfiguredEntrypointModuleId() { + final var aggregator = IRBackend.aggregator(); + aggregator.entryPointCallableName("frame"); + aggregator.entryPointModuleId(new ModuleId(3)); + + final var backend = aggregator.emit(); + assertEquals("frame", backend.getEntryPointCallableName()); + assertEquals(3, backend.getEntryPointModuleId().getIndex()); } @Test