requires explicit frame markers for executable PBS projects

This commit is contained in:
bQUARKz 2026-03-26 19:56:13 +00:00
parent 459f5e3b87
commit 14eaa0e0c0
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 81 additions and 34 deletions

View File

@ -152,9 +152,8 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
entryPointModuleId = wrapperNamesByModule.keySet().iterator().next();
entryPointCallableName = wrapperNamesByModule.get(entryPointModuleId);
} else {
final var legacyEntrypoint = resolveLegacyEntrypoint(baseBackend);
entryPointModuleId = legacyEntrypoint.moduleId();
entryPointCallableName = legacyEntrypoint.callableName();
entryPointModuleId = baseBackend.getEntryPointModuleId();
entryPointCallableName = baseBackend.getEntryPointCallableName();
}
final var sortedModuleIds = new ArrayList<ModuleId>();
@ -397,19 +396,6 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
return (int) value;
}
private LegacyEntrypoint resolveLegacyEntrypoint(final IRBackend backend) {
for (final var candidateName : List.of("frame", "main")) {
final var candidates = backend.getExecutableFunctions().stream()
.filter(function -> candidateName.equals(function.callableName()))
.filter(function -> function.moduleId() != null && !function.moduleId().isNone())
.toList();
if (candidates.size() == 1) {
return new LegacyEntrypoint(candidates.getFirst().moduleId(), candidateName);
}
}
return new LegacyEntrypoint(backend.getEntryPointModuleId(), backend.getEntryPointCallableName());
}
private Set<ModuleId> blockedModulesByDependency(
final Set<ModuleId> failedModuleIds,
final Map<ModuleId, Set<ModuleId>> dependenciesByModule) {
@ -440,9 +426,4 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
ModuleId moduleId,
p.studio.compiler.models.IRBackendFile irBackendFile) {
}
private record LegacyEntrypoint(
ModuleId moduleId,
String callableName) {
}
}

View File

@ -13,34 +13,31 @@ final class PbsProjectLifecycleSemanticsValidator {
final java.util.List<PbsParsedSourceFile> parsedSourceFiles,
final DiagnosticSink diagnostics) {
final var frames = new ArrayList<PbsAst.FunctionDecl>();
var sawLifecycleMarker = false;
var sawExecutableSource = false;
for (final var parsedSource : parsedSourceFiles) {
if (parsedSource.sourceKind() == SourceKind.SDK_INTERFACE) {
continue;
}
sawExecutableSource = true;
for (final var topDecl : parsedSource.ast().topDecls()) {
if (!(topDecl instanceof PbsAst.FunctionDecl functionDecl)) {
continue;
}
if (functionDecl.lifecycleMarker() == PbsAst.LifecycleMarker.NONE) {
continue;
}
sawLifecycleMarker = true;
if (functionDecl.lifecycleMarker() == PbsAst.LifecycleMarker.FRAME) {
frames.add(functionDecl);
}
}
}
if (!sawLifecycleMarker) {
if (!sawExecutableSource) {
return;
}
if (frames.isEmpty()) {
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_MISSING_PROJECT_FRAME.name(),
"Lifecycle-marked executable sources must declare exactly one [Frame] function",
"Executable projects must declare exactly one [Frame] function",
parsedSourceFiles.getFirst().ast().span());
return;
}

View File

@ -162,8 +162,17 @@ class PBSFrontendPhaseServiceTest {
fn caller() -> int {
return target();
}
[Frame]
fn frame() -> void {
caller();
return;
}
""");
Files.writeString(barrelB, """
pub fn caller() -> int;
pub fn frame() -> void;
""");
Files.writeString(barrelB, "pub fn caller() -> int;");
final var projectTable = new ProjectTable();
final var fileTable = new FileTable(1);
@ -221,11 +230,12 @@ class PBSFrontendPhaseServiceTest {
final var sourceFile = modulePath.resolve("source.pbs");
final var modBarrel = modulePath.resolve("mod.barrel");
Files.writeString(sourceFile, """
fn run() -> int { return 1; }
[Frame]
fn frame() -> void { return; }
fn sum(a: int, b: int) -> int { return a + b; }
""");
Files.writeString(modBarrel, """
pub fn run() -> int;
pub fn frame() -> void;
pub fn sum(a: int, b: int) -> int;
""");
@ -255,7 +265,51 @@ class PBSFrontendPhaseServiceTest {
assertTrue(diagnostics.isEmpty());
assertEquals(2, irBackend.getFunctions().size());
assertEquals(2, irBackend.getExecutableFunctions().size());
assertTrue(irBackend.getExecutableFunctions().stream().anyMatch(function -> "frame".equals(function.callableName())));
assertTrue(irBackend.getExecutableFunctions().stream().anyMatch(function ->
function.callableName().startsWith("__pbs.frame_wrapper$m")));
}
@Test
void shouldRequireExplicitFrameMarkerForExecutableProjects() throws IOException {
final var projectRoot = tempDir.resolve("project-missing-frame-marker");
final var sourceRoot = projectRoot.resolve("src");
Files.createDirectories(sourceRoot);
final var sourceFile = sourceRoot.resolve("main.pbs");
final var modBarrel = sourceRoot.resolve("mod.barrel");
Files.writeString(sourceFile, """
fn frame() -> void { return; }
""");
Files.writeString(modBarrel, "pub fn frame() -> void;");
final var projectTable = new ProjectTable();
final var fileTable = new FileTable(1);
final var projectId = projectTable.register(ProjectDescriptor.builder()
.rootPath(projectRoot)
.name("main")
.version("1.0.0")
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
.build());
registerFile(projectId, projectRoot, sourceFile, fileTable);
registerFile(projectId, projectRoot, modBarrel, fileTable);
final var ctx = new FrontendPhaseContext(
projectTable,
fileTable,
new BuildStack(ReadOnlyList.wrap(List.of(projectId))));
final var diagnostics = DiagnosticSink.empty();
new PBSFrontendPhaseService().compile(
ctx,
diagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_MISSING_PROJECT_FRAME.name())),
diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
}
@Test
@ -720,6 +774,7 @@ class PBSFrontendPhaseServiceTest {
Files.writeString(sourceFile, """
import { Gfx } from @sdk:gfx;
[Frame]
fn frame() -> void
{
Gfx.clear_565(6577);
@ -769,12 +824,22 @@ class PBSFrontendPhaseServiceTest {
Files.writeString(sourceFile, """
import { Gfx } from @sdk:gfx;
fn frame() -> int
fn render() -> int
{
return Gfx.set_sprite(2, 0, 12, 18, 7, 3, true, false, true, 1);
}
[Frame]
fn frame() -> void
{
render();
return;
}
""");
Files.writeString(modBarrel, """
pub fn render() -> int;
pub fn frame() -> void;
""");
Files.writeString(modBarrel, "pub fn frame() -> int;");
final var projectTable = new ProjectTable();
final var fileTable = new FileTable(1);
@ -1099,6 +1164,7 @@ class PBSFrontendPhaseServiceTest {
import { Log } from @sdk:log;
import { Input } from @sdk:input;
[Frame]
fn frame() -> void
{
if (Input.pad().a().pressed())
@ -1182,6 +1248,7 @@ class PBSFrontendPhaseServiceTest {
Files.writeString(sourceFile, """
import { Input } from @sdk:input;
[Frame]
fn frame() -> void
{
Input.pad().x().pressed();
@ -1246,6 +1313,7 @@ class PBSFrontendPhaseServiceTest {
Files.writeString(sourceFile, """
import { Input } from @sdk:input;
[Frame]
fn frame() -> void
{
let touch = Input.touch();

View File

@ -4,6 +4,7 @@ import { Log } from @sdk:log;
import { Input } from @sdk:input;
import { Gfx } from @sdk:gfx;
[Frame]
fn frame() -> void
{
let touch = Input.touch();