implements PR009

This commit is contained in:
bQUARKz 2026-03-05 16:08:07 +00:00
parent 20c5cd7841
commit 63b6ad68c4
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
21 changed files with 456 additions and 203 deletions

View File

@ -1,36 +0,0 @@
# PR-009 - PBS Diagnostics Contract V1
## Briefing
O modelo atual de `Diagnostic` nao contem fase, template id nem placeholders estaveis, o que impede conformidade com o contrato de diagnosticos da spec.
Este PR eleva o contrato de diagnosticos para v1 sem acoplar a UI.
## Target
- Specs:
- `docs/pbs/specs/12. Diagnostics Specification.md` (secoes 6, 7, 8, 9, 10)
- `docs/pbs/specs/13. Lowering IRBackend Specification.md` (secao 7)
- Codigo:
- `prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/diagnostics/Diagnostic.java`
- `prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/diagnostics/DiagnosticSink.java`
- emissores de diagnostico no frontend PBS.
## Method
1. Estender `Diagnostic` com `phase`, `templateId` e `placeholders` nomeados.
2. Definir taxonomia minima de fases externas: `syntax`, `static-semantics`, `manifest-import-resolution`, `linking`, `host-admission`, `load-facing-rejection`.
3. Preservar `code`, `severity`, `span` e `related spans` como identidade estavel.
4. Introduzir helpers para emissao padronizada por fase/template.
5. Mapear erros atuais de lexer/parser/frontend para o novo contrato.
## Acceptance Criteria
- Todo diagnostico PBS emitido tem `code`, `severity`, `phase`, `templateId`, `primary span` e mensagem renderizada.
- Duplicatas/conflitos incluem ao menos um `related span` quando aplicavel.
- Identidade de diagnostico independe do texto localizado.
- Nenhuma fase obrigatoria e colapsada de forma opaca para o usuario.
- Chamadas legadas continuam compilando com caminho de migracao claro.
## Tests
- `DiagnosticSinkTest` e novos testes de contrato para:
- presenca obrigatoria de campos;
- estabilidade de `templateId/placeholders`;
- caso com `related span`;
- serializacao/`toString` com fase e codigo.
- Testes de integracao no frontend PBS garantindo fase correta por erro.

View File

@ -1,35 +0,0 @@
# PR-010 - PBS IRBackend Lowering Contract Alignment
## Briefing
O lowering atual reduz tudo a `IRFunction(name, arity, hasReturnType)` e perde informacao exigida pela spec de lowering.
Este PR alinha o frontend boundary para preservar identidade, superficie de retorno e atribuicao de origem.
## Target
- Specs:
- `docs/pbs/specs/13. Lowering IRBackend Specification.md` (secoes 5, 6, 7, 8)
- `docs/pbs/specs/11. AST Specification.md` (metadados obrigatorios)
- Codigo:
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java`
- `prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRFunction.java`
- `prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend*.java`
## Method
1. Expandir modelos de IR para preservar categoria de callable e superficie de retorno.
2. Propagar atribuicao de origem estavel (`file + span`) de forma obrigatoria.
3. Diferenciar rejeicao de forma nao suportada vs erro interno.
4. Garantir determinismo de ordem de emissao conforme ordem de fonte.
5. Manter bridge de compatibilidade para consumidores atuais de IR.
## Acceptance Criteria
- IRBackend preserva identidade e aridade de callables sem perda de categoria.
- Retorno (unit/plain/result) e mantido no boundary de frontend.
- Diagnosticos de lowering seguem contrato estavel (code/phase/template/span).
- Formas nao suportadas nao degradam silenciosamente para comportamento valido alternativo.
- Testes existentes de lowering continuam passando com adaptacao minima.
## Tests
- `PbsFrontendCompilerTest` ampliado para:
- preservacao de metadata no IR;
- ordem deterministica de callables;
- casos de rejeicao de lowering com diagnostico estavel.
- Novos testes para serializacao/debug de `IRBackend`.

View File

@ -1,35 +0,0 @@
# PR-011 - PBS Gate U Conformance Fixtures
## Briefing
A spec exige evidencia de conformidade (Gate U), mas o frontend ainda tem poucos testes unitarios sem fixture matrix.
Este PR cria a infraestrutura e a bateria minima de fixtures para validar lexer, parser, semantica, diagnosticos e lowering.
## Target
- Specs:
- `docs/pbs/specs/11. AST Specification.md` (secao 12)
- `docs/pbs/specs/13. Lowering IRBackend Specification.md` (secao 8)
- `docs/general/specs/13. Conformance Test Specification.md`
- Codigo:
- novo pacote de teste/fixtures em `prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/`.
## Method
1. Introduzir harness de fixture com entrada `.pbs/.barrel` + expectativa estruturada.
2. Cobrir casos validos das familias obrigatorias de AST e fluxo de parse.
3. Cobrir casos invalidos obrigatorios (missing closer, non-assoc chains, formas fora de slice).
4. Adicionar asserts de spans e identidade de diagnostico (code/phase/template).
5. Integrar suite no gradle test da frontend PBS.
## Acceptance Criteria
- Existe fixture matrix positiva e negativa para lexer/parser/semantica/lowering.
- Gate U cobre familias obrigatorias de declaracao, statement e expressao.
- Casos de recovery garantem AST coerente mesmo com erro.
- Conformidade e verificavel de forma reproduzivel em CI.
- Falhas mostram diff util entre esperado e obtido.
## Tests
- `PbsConformanceFixtureTest` novo com subconjuntos:
- `valid/` para parse e lowering;
- `invalid/syntax/`;
- `invalid/static-semantics/`;
- `invalid/lowering/`.
- Execucao via `./gradlew :prometeu-compiler:frontends:prometeu-frontend-pbs:test`.

View File

@ -270,7 +270,7 @@ public final class PbsLexer {
final String message,
final long spanStart,
final long spanEnd) {
diagnostics.error(lexErrors.name(), message, new Span(fileId, spanStart, spanEnd));
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, lexErrors.name(), message, new Span(fileId, spanStart, spanEnd));
}
private static Map<String, PbsTokenKind> buildKeywords() {

View File

@ -43,7 +43,7 @@ public final class PbsModuleVisibilityValidator {
if (module.sourceFiles().isEmpty()) {
if (!module.barrelFiles().isEmpty()) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_BARREL_WITHOUT_SOURCE.name(),
"Module %s has mod.barrel but no .pbs source files".formatted(displayModule(module.coordinates())),
module.barrelFiles().getFirst().ast().span());
@ -52,7 +52,7 @@ public final class PbsModuleVisibilityValidator {
}
if (module.barrelFiles().isEmpty()) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_MISSING_BARREL.name(),
"Module %s is missing mod.barrel".formatted(displayModule(module.coordinates())),
module.sourceFiles().getFirst().ast().span());
@ -63,7 +63,7 @@ public final class PbsModuleVisibilityValidator {
final var first = module.barrelFiles().getFirst();
for (int i = 1; i < module.barrelFiles().size(); i++) {
final var duplicate = module.barrelFiles().get(i);
diagnostics.report(
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsLinkErrors.E_LINK_DUPLICATE_BARREL_FILE.name(),
"Module %s has multiple mod.barrel files".formatted(displayModule(module.coordinates())),
@ -74,8 +74,8 @@ public final class PbsModuleVisibilityValidator {
final var barrel = module.barrelFiles().getFirst();
final var declarations = collectDeclarations(module, nameTable);
final Set<NonFunctionSymbolKey> seenNonFunctionEntries = new HashSet<>();
final Set<FunctionSymbolKey> seenFunctionEntries = new HashSet<>();
final Map<NonFunctionSymbolKey, Span> seenNonFunctionEntries = new HashMap<>();
final Map<FunctionSymbolKey, Span> seenFunctionEntries = new HashMap<>();
for (final var item : barrel.ast().items()) {
if (item instanceof PbsAst.BarrelFunctionItem functionItem) {
@ -86,19 +86,21 @@ public final class PbsModuleVisibilityValidator {
functionItem.returnType(),
functionItem.resultErrorType(),
nameTable);
if (!seenFunctionEntries.add(functionKey)) {
diagnostics.error(
final var firstFunctionEntry = seenFunctionEntries.putIfAbsent(functionKey, functionItem.span());
if (firstFunctionEntry != null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_DUPLICATE_BARREL_ENTRY.name(),
"Duplicate barrel function entry '%s' in module %s".formatted(
functionItem.name(),
displayModule(module.coordinates())),
functionItem.span());
functionItem.span(),
List.of(new RelatedSpan("First barrel function entry is here", firstFunctionEntry)));
continue;
}
final var matches = declarations.functionsBySignature.getOrDefault(functionKey, List.of());
if (matches.isEmpty()) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name(),
"Barrel function entry '%s' in module %s does not resolve to a declaration".formatted(
functionItem.name(),
@ -107,12 +109,13 @@ public final class PbsModuleVisibilityValidator {
continue;
}
if (matches.size() > 1) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_AMBIGUOUS_BARREL_ENTRY.name(),
"Barrel function entry '%s' in module %s resolves ambiguously".formatted(
functionItem.name(),
displayModule(module.coordinates())),
functionItem.span());
functionItem.span(),
List.of(new RelatedSpan("One matching function declaration is here", matches.getFirst())));
continue;
}
@ -128,19 +131,21 @@ public final class PbsModuleVisibilityValidator {
}
final var symbolKey = new NonFunctionSymbolKey(symbolKind, nameTable.register(nonFunctionName(item)));
if (!seenNonFunctionEntries.add(symbolKey)) {
diagnostics.error(
final var firstNonFunctionEntry = seenNonFunctionEntries.putIfAbsent(symbolKey, item.span());
if (firstNonFunctionEntry != null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_DUPLICATE_BARREL_ENTRY.name(),
"Duplicate barrel entry '%s' in module %s".formatted(
nonFunctionName(item),
displayModule(module.coordinates())),
item.span());
item.span(),
List.of(new RelatedSpan("First barrel entry is here", firstNonFunctionEntry)));
continue;
}
final var matches = declarations.nonFunctionsByKindAndName.getOrDefault(symbolKey, List.of());
if (matches.isEmpty()) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name(),
"Barrel entry '%s' in module %s does not resolve to a declaration".formatted(
nonFunctionName(item),
@ -149,12 +154,13 @@ public final class PbsModuleVisibilityValidator {
continue;
}
if (matches.size() > 1) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_AMBIGUOUS_BARREL_ENTRY.name(),
"Barrel entry '%s' in module %s resolves ambiguously".formatted(
nonFunctionName(item),
displayModule(module.coordinates())),
item.span());
item.span(),
List.of(new RelatedSpan("One matching declaration is here", matches.getFirst())));
continue;
}
@ -188,7 +194,7 @@ public final class PbsModuleVisibilityValidator {
for (final var importItem : importDecl.items()) {
final var importedNameId = nameTable.register(importItem.name());
if (!targetExports.publicNameIds.contains(importedNameId)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_IMPORT_SYMBOL_NOT_PUBLIC.name(),
"Symbol '%s' is not public in module %s".formatted(
importItem.name(),

View File

@ -303,7 +303,7 @@ public final class PbsBarrelParser {
}
private void report(final PbsToken token, final ParseErrors parseErrors, final String message) {
diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end()));
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, parseErrors.name(), message, new Span(fileId, token.start(), token.end()));
}
private record ParsedReturnSpec(

View File

@ -630,7 +630,7 @@ final class PbsExprParser {
}
private void report(final PbsToken token, final ParseErrors parseErrors, final String message) {
diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end()));
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, parseErrors.name(), message, new Span(fileId, token.start(), token.end()));
}
private long parseLongOrDefault(final String text) {

View File

@ -5,11 +5,13 @@ import p.studio.compiler.pbs.lexer.PbsToken;
import p.studio.compiler.pbs.lexer.PbsTokenKind;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.diagnostics.RelatedSpan;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
/**
* High-level manual parser for PBS source files.
@ -279,7 +281,7 @@ public final class PbsParser {
private void parseRejectedAttributeList() {
while (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
final var attributeSpan = parseAttribute();
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
ParseErrors.E_PARSE_ATTRIBUTES_NOT_ALLOWED.name(),
"Attributes are not allowed in ordinary .pbs source modules",
attributeSpan);
@ -448,32 +450,44 @@ public final class PbsParser {
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after enum name");
final var cases = new ArrayList<PbsAst.EnumCase>();
final var caseLabels = new HashSet<String>();
final var explicitCaseIds = new HashSet<Long>();
final var caseLabels = new HashMap<String, Span>();
final var explicitCaseIds = new HashMap<Long, Span>();
var hasExplicitCases = false;
var hasImplicitCases = false;
if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
do {
final var caseName = consume(PbsTokenKind.IDENTIFIER, "Expected enum case label");
final var caseNameSpan = span(caseName.start(), caseName.end());
Long explicitValue = null;
if (cursor.match(PbsTokenKind.EQUAL)) {
final var intToken = consume(PbsTokenKind.INT_LITERAL, "Expected integer literal after '=' in enum case");
final var intTokenSpan = span(intToken.start(), intToken.end());
explicitValue = parseLongOrNull(intToken.lexeme());
hasExplicitCases = true;
if (explicitValue == null) {
report(intToken, ParseErrors.E_PARSE_INVALID_ENUM_FORM,
"Invalid explicit enum identifier");
} else if (!explicitCaseIds.add(explicitValue)) {
report(intToken, ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_ID,
"Duplicate explicit enum identifier '%s'".formatted(explicitValue));
} else {
final var firstIdSpan = explicitCaseIds.putIfAbsent(explicitValue, intTokenSpan);
if (firstIdSpan != null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_ID.name(),
"Duplicate explicit enum identifier '%s'".formatted(explicitValue),
intTokenSpan,
List.of(new RelatedSpan("First explicit enum identifier is here", firstIdSpan)));
}
}
} else {
hasImplicitCases = true;
}
if (!caseLabels.add(caseName.lexeme())) {
report(caseName, ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_LABEL,
"Duplicate enum case label '%s'".formatted(caseName.lexeme()));
final var firstLabelSpan = caseLabels.putIfAbsent(caseName.lexeme(), caseNameSpan);
if (firstLabelSpan != null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_LABEL.name(),
"Duplicate enum case label '%s'".formatted(caseName.lexeme()),
caseNameSpan,
List.of(new RelatedSpan("First enum case label is here", firstLabelSpan)));
}
cases.add(new PbsAst.EnumCase(caseName.lexeme(), explicitValue, span(caseName.start(), cursor.previous().end())));
@ -1162,7 +1176,7 @@ public final class PbsParser {
* Reports a parser diagnostic at the given token span.
*/
private void report(final PbsToken token, final ParseErrors parseErrors, final String message) {
diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end()));
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, parseErrors.name(), message, new Span(fileId, token.start(), token.end()));
}
private record ParsedReturnSpec(

View File

@ -46,7 +46,7 @@ final class PbsDeclarationRuleValidator {
final var parameterNameId = nameTable.register(parameter.name());
final var first = seen.putIfAbsent(parameterNameId, parameter.span());
if (first != null) {
diagnostics.report(
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_PARAMETER_NAME.name(),
"Duplicate parameter name '%s' in %s".formatted(parameter.name(), ownerDescription),
@ -68,7 +68,7 @@ final class PbsDeclarationRuleValidator {
final var caseNameId = nameTable.register(caseName);
final var first = seenCases.putIfAbsent(caseNameId, errorDecl.span());
if (first != null) {
diagnostics.report(
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_ERROR_CASE_LABEL.name(),
"Duplicate error case label '%s' in '%s'".formatted(caseName, errorDecl.name()),
@ -85,7 +85,7 @@ final class PbsDeclarationRuleValidator {
final var labelId = nameTable.register(enumCase.name());
final var firstLabel = seenLabels.putIfAbsent(labelId, enumCase.span());
if (firstLabel != null) {
diagnostics.report(
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_LABEL.name(),
"Duplicate enum case label '%s' in '%s'".formatted(enumCase.name(), enumDecl.name()),
@ -96,7 +96,7 @@ final class PbsDeclarationRuleValidator {
if (enumCase.explicitValue() != null) {
final var firstId = seenIds.putIfAbsent(enumCase.explicitValue(), enumCase.span());
if (firstId != null) {
diagnostics.report(
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_ID.name(),
"Duplicate enum case id '%s' in '%s'".formatted(enumCase.explicitValue(), enumDecl.name()),
@ -123,7 +123,7 @@ final class PbsDeclarationRuleValidator {
void validateConstDeclaration(final PbsAst.ConstDecl constDecl) {
if (constDecl.explicitType() == null) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_MISSING_CONST_TYPE_ANNOTATION.name(),
"Const declaration '%s' must include an explicit type annotation".formatted(constDecl.name()),
constDecl.span());
@ -135,7 +135,7 @@ final class PbsDeclarationRuleValidator {
}
if (constDecl.initializer() == null) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_MISSING_CONST_INITIALIZER.name(),
"Non-builtin const declaration '%s' must include an initializer".formatted(constDecl.name()),
constDecl.span());
@ -149,7 +149,7 @@ final class PbsDeclarationRuleValidator {
final String ownerDescription,
final Span span) {
if (returnKind == PbsAst.ReturnKind.RESULT && PbsTypeSurfaceSemanticsValidator.isOptionalType(returnType)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MIXED_OPTIONAL_RESULT_RETURN.name(),
"Return surface for %s cannot combine 'result' with top-level 'optional' payload".formatted(ownerDescription),
span);
@ -175,7 +175,7 @@ final class PbsDeclarationRuleValidator {
final var labelId = nameTable.register(field.label());
final var first = seen.putIfAbsent(labelId, field.span());
if (first != null) {
diagnostics.report(
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_RETURN_LABEL.name(),
"Duplicate return label '%s' in %s".formatted(field.label(), ownerDescription),

View File

@ -106,7 +106,7 @@ public final class PbsDeclarationSemanticsValidator {
binder.registerCtor(ctorScope, ctor.name(), PbsCallableShape.ctorShapeKey(ctor.parameters()), ctor.span());
if (PbsCtorReturnScanner.containsReturnStatement(ctor.body())) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_RETURN_INSIDE_CTOR.name(),
"Constructors cannot contain 'return' statements",
ctor.span());

View File

@ -170,7 +170,7 @@ final class PbsFlowBodyAnalyzer {
true,
this::analyzeBlock).type();
if (!typeOps.isBool(condition)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_IF_NON_BOOL_CONDITION.name(),
"If statement condition must have bool type",
ifStatement.condition().span());
@ -215,7 +215,7 @@ final class PbsFlowBodyAnalyzer {
true,
this::analyzeBlock).type();
if (!typeOps.compatible(fromType, iteratorType) || !typeOps.compatible(untilType, iteratorType)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_FOR_TYPE_MISMATCH.name(),
"For-loop bounds must match declared iterator type",
span);
@ -234,7 +234,7 @@ final class PbsFlowBodyAnalyzer {
true,
this::analyzeBlock).type();
if (!typeOps.compatible(stepType, iteratorType)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_FOR_TYPE_MISMATCH.name(),
"For-loop step must match declared iterator type",
stepExpression.span());
@ -259,7 +259,7 @@ final class PbsFlowBodyAnalyzer {
true,
this::analyzeBlock).type();
if (!typeOps.isBool(condition)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_WHILE_NON_BOOL_CONDITION.name(),
"While condition must have bool type",
whileStatement.condition().span());

View File

@ -265,7 +265,7 @@ final class PbsFlowExpressionAnalyzer {
ExprUse.VALUE,
true).type();
if (!typeOps.isBool(condition)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_IF_NON_BOOL_CONDITION.name(),
"If expression condition must have bool type",
ifExpr.condition().span());
@ -285,7 +285,7 @@ final class PbsFlowExpressionAnalyzer {
true).type();
if (!typeOps.compatible(thenType, elseType)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_IF_BRANCH_TYPE_MISMATCH.name(),
"If expression branches must have compatible types",
ifExpr.span());
@ -318,7 +318,7 @@ final class PbsFlowExpressionAnalyzer {
ExprUse.VALUE,
true).type();
if (optional.kind() != Kind.OPTIONAL) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_ELSE_NON_OPTIONAL_LEFT.name(),
"Left operand of 'else' must have optional type",
elseExpr.optionalExpression().span());
@ -349,7 +349,7 @@ final class PbsFlowExpressionAnalyzer {
ExprUse.VALUE,
true).type();
if (!typeOps.compatible(fallback, payload)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_ELSE_FALLBACK_TYPE_MISMATCH.name(),
"Fallback expression in 'else' must match optional payload type",
elseExpr.fallbackExpression().span());
@ -369,7 +369,7 @@ final class PbsFlowExpressionAnalyzer {
ExprUse.VALUE,
true).type();
if (source.kind() != Kind.RESULT) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_PROPAGATE_NON_RESULT.name(),
"Propagation operator '!' requires result type",
propagateExpr.expression().span());
@ -377,7 +377,7 @@ final class PbsFlowExpressionAnalyzer {
}
final var sourceError = source.errorType();
if (resultErrorName == null || sourceError == null || !resultErrorName.equals(sourceError.name())) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_PROPAGATE_ERROR_MISMATCH.name(),
"Propagation operator '!' requires matching result error type in enclosing callable",
propagateExpr.span());
@ -396,7 +396,7 @@ final class PbsFlowExpressionAnalyzer {
}
if (expression instanceof PbsAst.NoneExpr noneExpr) {
if (expectedType == null || expectedType.kind() != Kind.OPTIONAL) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_NONE_WITHOUT_EXPECTED_OPTIONAL.name(),
"'none' requires an expected optional type context",
noneExpr.span());
@ -465,12 +465,12 @@ final class PbsFlowExpressionAnalyzer {
compatible.add(candidate);
}
if (compatible.size() > 1) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_APPLY_AMBIGUOUS_OVERLOAD.name(),
"Bind target resolves ambiguously for callback type",
bindExpr.span());
} else if (compatible.isEmpty()) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_APPLY_UNRESOLVED_OVERLOAD.name(),
"Bind target does not resolve for callback type",
bindExpr.span());
@ -636,7 +636,7 @@ final class PbsFlowExpressionAnalyzer {
final DiagnosticSink diagnostics) {
final var candidates = callee.callables();
if (candidates.isEmpty()) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_APPLY_NON_CALLABLE_TARGET.name(),
"Apply/call target is not callable",
calleeSpan);
@ -651,7 +651,7 @@ final class PbsFlowExpressionAnalyzer {
}
if (compatible.isEmpty()) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_APPLY_UNRESOLVED_OVERLOAD.name(),
"No callable overload matches the provided argument",
wholeSpan);
@ -666,7 +666,7 @@ final class PbsFlowExpressionAnalyzer {
return ExprResult.type(narrowed.getFirst().outputType());
}
if (!narrowed.isEmpty()) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_APPLY_AMBIGUOUS_OVERLOAD.name(),
"Callable overload resolution remains ambiguous",
wholeSpan);
@ -675,7 +675,7 @@ final class PbsFlowExpressionAnalyzer {
}
if (compatible.size() > 1) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_APPLY_AMBIGUOUS_OVERLOAD.name(),
"Callable overload resolution is ambiguous",
wholeSpan);
@ -711,7 +711,7 @@ final class PbsFlowExpressionAnalyzer {
if (enumCases != null && enumCases.contains(memberExpr.memberName())) {
return ExprResult.type(TypeView.enumType(receiver.name()));
}
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Invalid type member access",
memberExpr.span());
@ -720,7 +720,7 @@ final class PbsFlowExpressionAnalyzer {
if (receiver.kind() == Kind.TUPLE) {
if (receiver.tupleFields().size() <= 1) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Member projection is not defined for single-slot carrier values",
memberExpr.span());
@ -731,7 +731,7 @@ final class PbsFlowExpressionAnalyzer {
return ExprResult.type(field.type());
}
}
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Tuple label '%s' does not exist".formatted(memberExpr.memberName()),
memberExpr.span());
@ -750,7 +750,7 @@ final class PbsFlowExpressionAnalyzer {
final var methods = struct.methods().get(memberExpr.memberName());
if (methods != null && !methods.isEmpty()) {
if (use == ExprUse.VALUE) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_BARE_METHOD_EXTRACTION.name(),
"Bare method extraction is not allowed in PBS core",
memberExpr.span());
@ -758,7 +758,7 @@ final class PbsFlowExpressionAnalyzer {
}
return ExprResult.callables(methods, true);
}
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Struct member '%s' does not exist".formatted(memberExpr.memberName()),
memberExpr.span());
@ -773,7 +773,7 @@ final class PbsFlowExpressionAnalyzer {
final var methods = service.methods().get(memberExpr.memberName());
if (methods != null && !methods.isEmpty()) {
if (use == ExprUse.VALUE) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_BARE_METHOD_EXTRACTION.name(),
"Bare method extraction is not allowed in PBS core",
memberExpr.span());
@ -781,7 +781,7 @@ final class PbsFlowExpressionAnalyzer {
}
return ExprResult.callables(methods, true);
}
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Service member '%s' does not exist".formatted(memberExpr.memberName()),
memberExpr.span());
@ -796,7 +796,7 @@ final class PbsFlowExpressionAnalyzer {
final var methods = contract.methods().get(memberExpr.memberName());
if (methods != null && !methods.isEmpty()) {
if (use == ExprUse.VALUE) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_BARE_METHOD_EXTRACTION.name(),
"Bare method extraction is not allowed in PBS core",
memberExpr.span());
@ -804,14 +804,14 @@ final class PbsFlowExpressionAnalyzer {
}
return ExprResult.callables(methods, true);
}
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Contract member '%s' does not exist".formatted(memberExpr.memberName()),
memberExpr.span());
return ExprResult.type(TypeView.unknown());
}
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Invalid member access target",
memberExpr.span());
@ -842,7 +842,7 @@ final class PbsFlowExpressionAnalyzer {
final var selectorComparable = typeOps.isScalarComparable(selector) || selector.kind() == Kind.ENUM;
if (!selectorComparable) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_SWITCH_SELECTOR_INVALID.name(),
"Switch selector type is not supported",
switchExpr.selector().span());
@ -857,7 +857,7 @@ final class PbsFlowExpressionAnalyzer {
if (arm.pattern() instanceof PbsAst.WildcardSwitchPattern) {
hasWildcard = true;
} else if (!seenPatterns.add(patternKey)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_SWITCH_DUPLICATE_PATTERN.name(),
"Duplicate switch pattern",
arm.pattern().span());
@ -865,7 +865,7 @@ final class PbsFlowExpressionAnalyzer {
final var patternType = switchPatternType(arm.pattern(), model, diagnostics);
if (patternType != null && !typeOps.compatible(patternType, selector)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_SWITCH_PATTERN_TYPE_MISMATCH.name(),
"Switch pattern is not compatible with selector type",
arm.pattern().span());
@ -883,7 +883,7 @@ final class PbsFlowExpressionAnalyzer {
if (armType == null) {
armType = currentArmType;
} else if (!typeOps.compatible(currentArmType, armType)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_SWITCH_ARM_TYPE_MISMATCH.name(),
"Switch arm block types must be compatible",
arm.span());
@ -907,7 +907,7 @@ final class PbsFlowExpressionAnalyzer {
}
}
if (!exhaustive) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_SWITCH_NON_EXHAUSTIVE.name(),
"Switch expression in value position must be exhaustive",
switchExpr.span());
@ -952,7 +952,7 @@ final class PbsFlowExpressionAnalyzer {
final var caseName = segments.get(1);
final var enumCases = model.enums.get(enumName);
if (enumCases == null || !enumCases.contains(caseName)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_SWITCH_PATTERN_TYPE_MISMATCH.name(),
"Enum case pattern '%s.%s' does not resolve".formatted(enumName, caseName),
enumCaseSwitchPattern.span());
@ -998,14 +998,14 @@ final class PbsFlowExpressionAnalyzer {
ExprUse.VALUE,
true).type();
if (sourceType.kind() != Kind.RESULT) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_HANDLE_NON_RESULT.name(),
"Handle requires result expression",
handleExpr.value().span());
return TypeView.unknown();
}
if (resultErrorName == null) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(),
"Handle requires enclosing callable with result return",
handleExpr.span());
@ -1025,12 +1025,12 @@ final class PbsFlowExpressionAnalyzer {
final var errorName = segments.getFirst();
final var caseName = segments.get(1);
if (!errorName.equals(sourceErrorName) || !sourceCases.contains(caseName)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(),
"Handle arm pattern does not match source result error type",
arm.pattern().span());
} else if (!matchedCases.add(caseName)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(),
"Handle arm duplicates same error case pattern",
arm.pattern().span());
@ -1040,7 +1040,7 @@ final class PbsFlowExpressionAnalyzer {
if (arm.remapTarget() != null) {
if (!matchesTargetError(arm.remapTarget(), resultErrorName, model)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(),
"Handle remap target must match enclosing callable result error type",
arm.remapTarget().span());
@ -1051,7 +1051,7 @@ final class PbsFlowExpressionAnalyzer {
}
if (!hasWildcard && !sourceCases.isEmpty() && !matchedCases.containsAll(sourceCases)) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(),
"Handle mapping is not exhaustive for source result error cases",
handleExpr.span());

View File

@ -50,7 +50,7 @@ final class PbsNamespaceBinder {
return;
}
diagnostics.report(
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name(),
"Duplicate callable declaration '%s' with shape %s in %s".formatted(callableName, shape, scope.label()),
@ -70,7 +70,7 @@ final class PbsNamespaceBinder {
return;
}
diagnostics.report(
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name(),
"Duplicate constructor declaration '%s' with shape %s in %s".formatted(ctorName, shape, scope.label()),
@ -90,7 +90,7 @@ final class PbsNamespaceBinder {
return;
}
diagnostics.report(
p.studio.compiler.source.diagnostics.Diagnostics.report(diagnostics,
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_DECLARATION.name(),
"Duplicate %s declaration '%s' in %s".formatted(declarationKind, declarationName, namespaceName),

View File

@ -24,7 +24,7 @@ final class PbsTypeSurfaceSemanticsValidator {
switch (typeRef.kind()) {
case OPTIONAL -> {
if (typeRef.inner() == null || typeRef.inner().kind() == PbsAst.TypeRefKind.UNIT) {
diagnostics.error(
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE.name(),
"Invalid optional type surface in %s: 'optional void' is not allowed".formatted(ownerDescription),
typeRef.span());

View File

@ -63,7 +63,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
final var barrelAst = parseBarrelFile(fId, utf8Content, diagnostics);
moduleUnit.barrels.add(new PbsModuleVisibilityValidator.BarrelFile(fId, barrelAst));
} else {
diagnostics.error(
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));

View File

@ -0,0 +1,83 @@
package p.studio.compiler.pbs;
import org.junit.jupiter.api.Test;
import p.studio.compiler.pbs.lexer.LexErrors;
import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.pbs.linking.PbsLinkErrors;
import p.studio.compiler.pbs.linking.PbsModuleVisibilityValidator;
import p.studio.compiler.pbs.parser.PbsBarrelParser;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
import p.studio.compiler.source.diagnostics.DiagnosticPhase;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsDiagnosticsContractTest {
@Test
void shouldTagLexerDiagnosticsWithSyntaxPhase() {
final var diagnostics = DiagnosticSink.empty();
PbsLexer.lex("$", new FileId(0), diagnostics);
final var diagnostic = diagnostics.stream()
.filter(d -> d.getCode().equals(LexErrors.E_LEX_INVALID_CHAR.name()))
.findFirst()
.orElseThrow();
assertEquals(DiagnosticPhase.SYNTAX, diagnostic.getPhase());
assertEquals(diagnostic.getCode(), diagnostic.getTemplateId());
assertTrue(diagnostic.getPlaceholders().isEmpty());
}
@Test
void shouldTagSemanticsDiagnosticsWithStaticSemanticsPhase() {
final var source = """
fn sum(a: int) {}
fn sum(a: int) {}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
final var diagnostic = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name()))
.findFirst()
.orElseThrow();
assertEquals(DiagnosticPhase.STATIC_SEMANTICS, diagnostic.getPhase());
}
@Test
void shouldTagLinkingDiagnosticsAndExposeRelatedSpanForDuplicateBarrelEntry() {
final var diagnostics = DiagnosticSink.empty();
final var sourceFileId = new FileId(1);
final var barrelFileId = new FileId(2);
final var source = """
fn run() {}
""";
final var barrel = """
pub fn run();
pub fn run();
""";
final var sourceAst = PbsParser.parse(PbsLexer.lex(source, sourceFileId, diagnostics), sourceFileId, diagnostics);
final var barrelAst = PbsBarrelParser.parse(PbsLexer.lex(barrel, barrelFileId, diagnostics), barrelFileId, diagnostics);
final var module = new PbsModuleVisibilityValidator.ModuleUnit(
new PbsModuleVisibilityValidator.ModuleCoordinates("app", ReadOnlyList.wrap(List.of("core"))),
ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.SourceFile(sourceFileId, sourceAst))),
ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.BarrelFile(barrelFileId, barrelAst))));
new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(module)), diagnostics);
final var diagnostic = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsLinkErrors.E_LINK_DUPLICATE_BARREL_ENTRY.name()))
.findFirst()
.orElseThrow();
assertEquals(DiagnosticPhase.LINKING, diagnostic.getPhase());
assertEquals(1, diagnostic.getRelated().size());
}
}

View File

@ -5,11 +5,16 @@ import p.studio.compiler.source.Span;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Getter
public class Diagnostic {
private final Severity severity;
private final String code;
private final DiagnosticPhase phase;
private final String templateId;
private final Map<String, String> placeholders;
private final String message;
private final Span span;
private final ReadOnlyList<RelatedSpan> related;
@ -20,10 +25,43 @@ public class Diagnostic {
final String message,
final Span span,
final List<RelatedSpan> related) {
this.severity = severity;
this.code = code;
this.message = message;
this.span = span;
this.related = ReadOnlyList.wrap(related);
this(
severity,
code,
DiagnosticPhase.fromCode(code),
code,
Map.of(),
message,
span,
related);
}
public Diagnostic(
final Severity severity,
final String code,
final DiagnosticPhase phase,
final String templateId,
final Map<String, String> placeholders,
final String message,
final Span span,
final List<RelatedSpan> related) {
this.severity = Objects.requireNonNull(severity);
this.code = Objects.requireNonNull(code);
this.phase = Objects.requireNonNull(phase);
this.templateId = Objects.requireNonNull(templateId);
this.placeholders = Map.copyOf(Objects.requireNonNull(placeholders));
this.message = Objects.requireNonNull(message);
this.span = Objects.requireNonNull(span);
this.related = ReadOnlyList.wrap(List.copyOf(Objects.requireNonNull(related)));
}
@Override
public String toString() {
return "%s[%s:%s] %s @ %s".formatted(
severity,
phase.id(),
code,
message,
span);
}
}

View File

@ -0,0 +1,53 @@
package p.studio.compiler.source.diagnostics;
import java.util.Locale;
public enum DiagnosticPhase {
SYNTAX("syntax"),
STATIC_SEMANTICS("static-semantics"),
MANIFEST_IMPORT_RESOLUTION("manifest-import-resolution"),
LINKING("linking"),
HOST_ADMISSION("host-admission"),
LOAD_FACING_REJECTION("load-facing-rejection"),
UNKNOWN("unknown"),
;
private final String id;
DiagnosticPhase(final String id) {
this.id = id;
}
public String id() {
return id;
}
public static DiagnosticPhase fromCode(final String code) {
if (code == null || code.isBlank()) {
return UNKNOWN;
}
final var normalized = code.toUpperCase(Locale.ROOT);
if (normalized.startsWith("E_PARSE_") || normalized.startsWith("E_LEX_")
|| normalized.startsWith("W_PARSE_") || normalized.startsWith("W_LEX_")) {
return SYNTAX;
}
if (normalized.startsWith("E_SEM_") || normalized.startsWith("W_SEM_")) {
return STATIC_SEMANTICS;
}
if (normalized.startsWith("E_LINK_") || normalized.startsWith("W_LINK_")) {
return LINKING;
}
if (normalized.startsWith("E_MANIFEST_") || normalized.startsWith("W_MANIFEST_")
|| normalized.startsWith("E_IMPORT_") || normalized.startsWith("W_IMPORT_")) {
return MANIFEST_IMPORT_RESOLUTION;
}
if (normalized.startsWith("E_HOST_") || normalized.startsWith("W_HOST_")) {
return HOST_ADMISSION;
}
if (normalized.startsWith("E_LOAD_") || normalized.startsWith("W_LOAD_")) {
return LOAD_FACING_REJECTION;
}
return UNKNOWN;
}
}

View File

@ -6,6 +6,7 @@ import p.studio.utilities.structures.ReadOnlyCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class DiagnosticSink implements ReadOnlyCollection<Diagnostic> {
@ -39,20 +40,29 @@ public class DiagnosticSink implements ReadOnlyCollection<Diagnostic> {
public DiagnosticSink report(
final Severity severity,
final String code,
final DiagnosticPhase phase,
final String templateId,
final Map<String, String> placeholders,
final String message,
final Span span) {
return report(severity, code, message, span, List.of());
return report(severity, code, phase, templateId, placeholders, message, span, List.of());
}
public DiagnosticSink report(
final Severity severity,
final String code,
final DiagnosticPhase phase,
final String templateId,
final Map<String, String> placeholders,
final String message,
final Span span,
final List<RelatedSpan> related) {
return report(new Diagnostic(
Objects.requireNonNull(severity),
Objects.requireNonNull(code),
Objects.requireNonNull(phase),
Objects.requireNonNull(templateId),
Map.copyOf(Objects.requireNonNull(placeholders)),
Objects.requireNonNull(message),
Objects.requireNonNull(span),
List.copyOf(Objects.requireNonNull(related))));
@ -60,16 +70,61 @@ public class DiagnosticSink implements ReadOnlyCollection<Diagnostic> {
public DiagnosticSink error(
final String code,
final String templateId,
final Map<String, String> placeholders,
final String message,
final Span span) {
return report(Severity.Error, code, message, span);
return report(Severity.Error, code, DiagnosticPhase.fromCode(code), templateId, placeholders, message, span);
}
public DiagnosticSink error(
final String code,
final String templateId,
final Map<String, String> placeholders,
final String message,
final Span span,
final List<RelatedSpan> related) {
return report(Severity.Error, code, DiagnosticPhase.fromCode(code), templateId, placeholders, message, span, related);
}
public DiagnosticSink error(
final DiagnosticPhase phase,
final String code,
final String templateId,
final Map<String, String> placeholders,
final String message,
final Span span) {
return report(Severity.Error, code, phase, templateId, placeholders, message, span);
}
public DiagnosticSink error(
final DiagnosticPhase phase,
final String code,
final String templateId,
final Map<String, String> placeholders,
final String message,
final Span span,
final List<RelatedSpan> related) {
return report(Severity.Error, code, phase, templateId, placeholders, message, span, related);
}
public DiagnosticSink warning(
final String code,
final String templateId,
final Map<String, String> placeholders,
final String message,
final Span span) {
return report(Severity.Warning, code, message, span);
return report(Severity.Warning, code, DiagnosticPhase.fromCode(code), templateId, placeholders, message, span);
}
public DiagnosticSink warning(
final DiagnosticPhase phase,
final String code,
final String templateId,
final Map<String, String> placeholders,
final String message,
final Span span) {
return report(Severity.Warning, code, phase, templateId, placeholders, message, span);
}
public DiagnosticSink merge(final DiagnosticSink diagnostics) {

View File

@ -0,0 +1,54 @@
package p.studio.compiler.source.diagnostics;
import p.studio.compiler.source.Span;
import java.util.List;
import java.util.Map;
public final class Diagnostics {
private Diagnostics() {
}
public static DiagnosticSink error(
final DiagnosticSink sink,
final String code,
final String message,
final Span span) {
return sink.error(code, code, Map.of(), message, span);
}
public static DiagnosticSink error(
final DiagnosticSink sink,
final String code,
final String message,
final Span span,
final List<RelatedSpan> related) {
return sink.error(code, code, Map.of(), message, span, related);
}
public static DiagnosticSink warning(
final DiagnosticSink sink,
final String code,
final String message,
final Span span) {
return sink.warning(code, code, Map.of(), message, span);
}
public static DiagnosticSink report(
final DiagnosticSink sink,
final Severity severity,
final String code,
final String message,
final Span span,
final List<RelatedSpan> related) {
return sink.report(
severity,
code,
DiagnosticPhase.fromCode(code),
code,
Map.of(),
message,
span,
related);
}
}

View File

@ -3,6 +3,9 @@ package p.studio.compiler.source.diagnostics;
import org.junit.jupiter.api.Test;
import p.studio.compiler.source.Span;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -12,8 +15,8 @@ class DiagnosticSinkTest {
void shouldReportAndCountDiagnostics() {
final var sink = DiagnosticSink.empty();
sink.error("E001", "Unknown symbol", Span.none());
sink.warning("W001", "Unused declaration", Span.none());
sink.error("E001", "E001", Map.of(), "Unknown symbol", Span.none());
sink.warning("W001", "W001", Map.of(), "Unused declaration", Span.none());
assertEquals(2, sink.size());
assertTrue(sink.hasErrors());
@ -25,9 +28,9 @@ class DiagnosticSinkTest {
@Test
void shouldMergeDiagnostics() {
final var a = DiagnosticSink.empty()
.warning("W010", "Unused import", Span.none());
.warning("W010", "W010", Map.of(), "Unused import", Span.none());
final var b = DiagnosticSink.empty()
.error("E999", "Invalid type", Span.none());
.error("E999", "E999", Map.of(), "Invalid type", Span.none());
a.merge(b);
@ -39,4 +42,57 @@ class DiagnosticSinkTest {
assertEquals(1, b.errorCount());
assertEquals(0, b.warningCount());
}
@Test
void shouldPopulateContractFieldsForLegacyEmission() {
final var sink = DiagnosticSink.empty();
sink.error("E_PARSE_EXPECTED_TOKEN", "E_PARSE_EXPECTED_TOKEN", Map.of(), "Expected token", Span.none());
final var diagnostic = sink.stream().findFirst().orElseThrow();
assertEquals("E_PARSE_EXPECTED_TOKEN", diagnostic.getCode());
assertEquals(DiagnosticPhase.SYNTAX, diagnostic.getPhase());
assertEquals("E_PARSE_EXPECTED_TOKEN", diagnostic.getTemplateId());
assertTrue(diagnostic.getPlaceholders().isEmpty());
assertEquals("Expected token", diagnostic.getMessage());
assertTrue(diagnostic.getSpan().isNone());
}
@Test
void shouldKeepExplicitPhaseTemplateAndPlaceholdersStable() {
final var sink = DiagnosticSink.empty();
sink.error(
DiagnosticPhase.LINKING,
"E_LINK_UNRESOLVED_BARREL_ENTRY",
"pbs.link.unresolved-barrel-entry",
Map.of("module", "@app:core", "symbol", "foo"),
"Barrel entry 'foo' does not resolve",
Span.none());
final var diagnostic = sink.stream().findFirst().orElseThrow();
assertEquals(DiagnosticPhase.LINKING, diagnostic.getPhase());
assertEquals("pbs.link.unresolved-barrel-entry", diagnostic.getTemplateId());
assertEquals(Map.of("module", "@app:core", "symbol", "foo"), diagnostic.getPlaceholders());
}
@Test
void shouldReportRelatedSpansAndRenderPhaseInToString() {
final var sink = DiagnosticSink.empty();
final var related = new RelatedSpan("First declaration is here", Span.none());
sink.error(
"E_SEM_DUPLICATE_DECLARATION",
"E_SEM_DUPLICATE_DECLARATION",
Map.of(),
"Duplicate declaration",
Span.none(),
List.of(related));
final var diagnostic = sink.stream().findFirst().orElseThrow();
assertEquals(1, diagnostic.getRelated().size());
assertEquals("First declaration is here", diagnostic.getRelated().getFirst().getMessage());
assertTrue(diagnostic.toString().contains("static-semantics"));
assertTrue(diagnostic.toString().contains("E_SEM_DUPLICATE_DECLARATION"));
}
}