implements PR020

This commit is contained in:
bQUARKz 2026-03-05 21:09:20 +00:00
parent eb469fd68c
commit d2287a0a58
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 198 additions and 6 deletions

View File

@ -21,17 +21,26 @@ public final class PbsFrontendCompiler {
final FileId fileId,
final String source,
final DiagnosticSink diagnostics) {
final var admissionBaseline = diagnostics.errorCount();
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
final var ast = PbsParser.parse(tokens, fileId, diagnostics);
return compileParsedFile(fileId, ast, diagnostics);
final var irBackendFile = compileParsedFile(fileId, ast, diagnostics);
if (diagnostics.errorCount() > admissionBaseline) {
return IRBackendFile.empty(fileId);
}
return irBackendFile;
}
public IRBackendFile compileParsedFile(
final FileId fileId,
final PbsAst.File ast,
final DiagnosticSink diagnostics) {
final var semanticsErrorBaseline = diagnostics.errorCount();
declarationSemanticsValidator.validate(ast, diagnostics);
flowSemanticsValidator.validate(ast, diagnostics);
if (diagnostics.errorCount() > semanticsErrorBaseline) {
return IRBackendFile.empty(fileId);
}
return new IRBackendFile(fileId, lowerFunctions(fileId, ast));
}

View File

@ -15,6 +15,7 @@ import p.studio.compiler.pbs.parser.PbsBarrelParser;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.diagnostics.DiagnosticPhase;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.utilities.structures.ReadOnlyList;
import p.studio.utilities.logs.LogAggregator;
@ -22,8 +23,11 @@ import p.studio.utilities.logs.LogAggregator;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@Slf4j
public class PBSFrontendPhaseService implements FrontendPhaseService {
@ -39,6 +43,8 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
final var irBackendAggregator = IRBackend.aggregator();
final var parsedSourceFiles = new ArrayList<ParsedSourceFile>();
final Map<PbsModuleVisibilityValidator.ModuleCoordinates, MutableModuleUnit> modulesByCoordinates = new LinkedHashMap<>();
final var moduleKeyByFile = new HashMap<FileId, String>();
final var failedModuleKeys = new HashSet<String>();
for (final var pId : ctx.stack.reverseTopologicalOrder) {
final var projectDescriptor = ctx.projectTable.get(pId);
@ -49,24 +55,36 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
sourceHandle.readUtf8().ifPresentOrElse(
utf8Content -> {
final var coordinates = resolveModuleCoordinates(projectDescriptor, sourceHandle);
final var moduleKey = moduleKey(coordinates);
final var moduleUnit = modulesByCoordinates.computeIfAbsent(
coordinates,
ignored -> new MutableModuleUnit());
switch (sourceHandle.getExtension()) {
case "pbs" -> {
moduleKeyByFile.put(fId, moduleKey);
final var parseErrorBaseline = diagnostics.errorCount();
final var ast = parseSourceFile(fId, utf8Content, diagnostics);
moduleUnit.sources.add(new PbsModuleVisibilityValidator.SourceFile(fId, ast));
parsedSourceFiles.add(new ParsedSourceFile(fId, ast));
parsedSourceFiles.add(new ParsedSourceFile(fId, ast, moduleKey));
if (diagnostics.errorCount() > parseErrorBaseline) {
failedModuleKeys.add(moduleKey);
}
}
case "barrel" -> {
moduleKeyByFile.put(fId, moduleKey);
if ("mod.barrel".equals(sourceHandle.getFilename())) {
final var parseErrorBaseline = diagnostics.errorCount();
final var barrelAst = parseBarrelFile(fId, utf8Content, diagnostics);
moduleUnit.barrels.add(new PbsModuleVisibilityValidator.BarrelFile(fId, barrelAst));
if (diagnostics.errorCount() > parseErrorBaseline) {
failedModuleKeys.add(moduleKey);
}
} else {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_INVALID_BARREL_FILENAME.name(),
"Only 'mod.barrel' is allowed as barrel filename",
new Span(fId, 0, utf8Content.getBytes(StandardCharsets.UTF_8).length));
failedModuleKeys.add(moduleKey);
}
}
default -> {
@ -89,8 +107,12 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
ReadOnlyList.wrap(moduleUnit.barrels)));
}
moduleVisibilityValidator.validate(ReadOnlyList.wrap(modules), diagnostics);
markModulesWithLinkingErrors(diagnostics, moduleKeyByFile, failedModuleKeys);
for (final var parsedSource : parsedSourceFiles) {
if (failedModuleKeys.contains(parsedSource.moduleKey())) {
continue;
}
final var irBackendFile = frontendCompiler.compileParsedFile(parsedSource.fileId(), parsedSource.ast(), diagnostics);
irBackendAggregator.merge(irBackendFile);
}
@ -145,6 +167,28 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
return sourceHandle.getRelativePath();
}
private void markModulesWithLinkingErrors(
final DiagnosticSink diagnostics,
final Map<FileId, String> moduleKeyByFile,
final Set<String> failedModuleKeys) {
for (final var diagnostic : diagnostics) {
if (!diagnostic.getSeverity().isError()) {
continue;
}
if (diagnostic.getPhase() != DiagnosticPhase.LINKING) {
continue;
}
final var moduleKey = moduleKeyByFile.get(diagnostic.getSpan().getFileId());
if (moduleKey != null) {
failedModuleKeys.add(moduleKey);
}
}
}
private String moduleKey(final PbsModuleVisibilityValidator.ModuleCoordinates coordinates) {
return coordinates.project() + ":" + String.join("/", coordinates.pathSegments().asList());
}
private static final class MutableModuleUnit {
private final ArrayList<PbsModuleVisibilityValidator.SourceFile> sources = new ArrayList<>();
private final ArrayList<PbsModuleVisibilityValidator.BarrelFile> barrels = new ArrayList<>();
@ -152,6 +196,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
private record ParsedSourceFile(
FileId fileId,
PbsAst.File ast) {
PbsAst.File ast,
String moduleKey) {
}
}

View File

@ -1,6 +1,7 @@
package p.studio.compiler.pbs;
import org.junit.jupiter.api.Test;
import p.studio.compiler.pbs.lexer.LexErrors;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
@ -44,10 +45,11 @@ class PbsFrontendCompilerTest {
final var diagnostics = DiagnosticSink.empty();
final var compiler = new PbsFrontendCompiler();
compiler.compileFile(new FileId(0), source, diagnostics);
final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
assertEquals(0, fileBackend.functions().size());
}
@Test
@ -59,9 +61,42 @@ class PbsFrontendCompilerTest {
final var diagnostics = DiagnosticSink.empty();
final var compiler = new PbsFrontendCompiler();
compiler.compileFile(new FileId(0), source, diagnostics);
final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().noneMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
assertEquals(2, fileBackend.functions().size());
}
@Test
void shouldNotLowerWhenSyntaxErrorsExist() {
final var source = """
$
fn run() -> int { return 1; }
""";
final var diagnostics = DiagnosticSink.empty();
final var compiler = new PbsFrontendCompiler();
final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(LexErrors.E_LEX_INVALID_CHAR.name())));
assertEquals(0, fileBackend.functions().size());
}
@Test
void shouldNotLowerWhenStaticSemanticsErrorsExist() {
final var source = """
fn run(x: int) -> int { return x; }
fn run(y: int) -> int { return y; }
""";
final var diagnostics = DiagnosticSink.empty();
final var compiler = new PbsFrontendCompiler();
final var fileBackend = compiler.compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
assertEquals(0, fileBackend.functions().size());
}
}

View File

@ -21,6 +21,7 @@ import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PBSFrontendPhaseServiceTest {
@ -61,13 +62,115 @@ class PBSFrontendPhaseServiceTest {
new BuildStack(ReadOnlyList.wrap(List.of(projectId))));
final var diagnostics = DiagnosticSink.empty();
new PBSFrontendPhaseService().compile(
final var irBackend = new PBSFrontendPhaseService().compile(
ctx,
diagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(PbsLinkErrors.E_LINK_INVALID_BARREL_FILENAME.name())));
assertEquals(0, irBackend.getFunctions().size());
}
@Test
void shouldLowerOnlyModulesThatPassAdmissionGates() throws IOException {
final var projectRoot = tempDir.resolve("project-mixed");
final var sourceRoot = projectRoot.resolve("src");
final var validModulePath = sourceRoot.resolve("valid");
final var invalidModulePath = sourceRoot.resolve("invalid");
Files.createDirectories(validModulePath);
Files.createDirectories(invalidModulePath);
final var validSource = validModulePath.resolve("source.pbs");
final var validBarrel = validModulePath.resolve("mod.barrel");
final var invalidSource = invalidModulePath.resolve("source.pbs");
Files.writeString(validSource, """
fn good(v: int) -> int {
return v;
}
""");
Files.writeString(validBarrel, "pub fn good(v: int) -> int;");
Files.writeString(invalidSource, """
fn bad(v: int) -> int {
return v;
}
""");
final var projectTable = new ProjectTable();
final var fileTable = new FileTable(1);
final var projectId = projectTable.register(ProjectDescriptor.builder()
.rootPath(projectRoot)
.name("core")
.version("1.0.0")
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
.build());
registerFile(projectId, projectRoot, validSource, fileTable);
registerFile(projectId, projectRoot, validBarrel, fileTable);
registerFile(projectId, projectRoot, invalidSource, fileTable);
final var ctx = new FrontendPhaseContext(
projectTable,
fileTable,
new BuildStack(ReadOnlyList.wrap(List.of(projectId))));
final var diagnostics = DiagnosticSink.empty();
final var irBackend = new PBSFrontendPhaseService().compile(
ctx,
diagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsLinkErrors.E_LINK_MISSING_BARREL.name())));
assertEquals(1, irBackend.getFunctions().size());
assertEquals("good", irBackend.getFunctions().getFirst().name());
}
@Test
void shouldLowerAllSourcesWhenNoAdmissionErrorsExist() throws IOException {
final var projectRoot = tempDir.resolve("project-valid");
final var sourceRoot = projectRoot.resolve("src");
final var modulePath = sourceRoot.resolve("math");
Files.createDirectories(modulePath);
final var sourceFile = modulePath.resolve("source.pbs");
final var modBarrel = modulePath.resolve("mod.barrel");
Files.writeString(sourceFile, """
fn run() -> int { return 1; }
fn sum(a: int, b: int) -> int { return a + b; }
""");
Files.writeString(modBarrel, """
pub fn run() -> int;
pub fn sum(a: int, b: int) -> int;
""");
final var projectTable = new ProjectTable();
final var fileTable = new FileTable(1);
final var projectId = projectTable.register(ProjectDescriptor.builder()
.rootPath(projectRoot)
.name("core")
.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();
final var irBackend = new PBSFrontendPhaseService().compile(
ctx,
diagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
assertTrue(diagnostics.isEmpty());
assertEquals(2, irBackend.getFunctions().size());
}
private void registerFile(