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 String message,
final long spanStart, final long spanStart,
final long spanEnd) { 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() { private static Map<String, PbsTokenKind> buildKeywords() {

View File

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

View File

@ -303,7 +303,7 @@ public final class PbsBarrelParser {
} }
private void report(final PbsToken token, final ParseErrors parseErrors, final String message) { 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( private record ParsedReturnSpec(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,7 +63,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
final var barrelAst = parseBarrelFile(fId, utf8Content, diagnostics); final var barrelAst = parseBarrelFile(fId, utf8Content, diagnostics);
moduleUnit.barrels.add(new PbsModuleVisibilityValidator.BarrelFile(fId, barrelAst)); moduleUnit.barrels.add(new PbsModuleVisibilityValidator.BarrelFile(fId, barrelAst));
} else { } else {
diagnostics.error( p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsLinkErrors.E_LINK_INVALID_BARREL_FILENAME.name(), PbsLinkErrors.E_LINK_INVALID_BARREL_FILENAME.name(),
"Only 'mod.barrel' is allowed as barrel filename", "Only 'mod.barrel' is allowed as barrel filename",
new Span(fId, 0, utf8Content.getBytes(StandardCharsets.UTF_8).length)); 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 p.studio.utilities.structures.ReadOnlyList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
@Getter @Getter
public class Diagnostic { public class Diagnostic {
private final Severity severity; private final Severity severity;
private final String code; private final String code;
private final DiagnosticPhase phase;
private final String templateId;
private final Map<String, String> placeholders;
private final String message; private final String message;
private final Span span; private final Span span;
private final ReadOnlyList<RelatedSpan> related; private final ReadOnlyList<RelatedSpan> related;
@ -20,10 +25,43 @@ public class Diagnostic {
final String message, final String message,
final Span span, final Span span,
final List<RelatedSpan> related) { final List<RelatedSpan> related) {
this.severity = severity; this(
this.code = code; severity,
this.message = message; code,
this.span = span; DiagnosticPhase.fromCode(code),
this.related = ReadOnlyList.wrap(related); 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.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
public class DiagnosticSink implements ReadOnlyCollection<Diagnostic> { public class DiagnosticSink implements ReadOnlyCollection<Diagnostic> {
@ -39,20 +40,29 @@ public class DiagnosticSink implements ReadOnlyCollection<Diagnostic> {
public DiagnosticSink report( public DiagnosticSink report(
final Severity severity, final Severity severity,
final String code, final String code,
final DiagnosticPhase phase,
final String templateId,
final Map<String, String> placeholders,
final String message, final String message,
final Span span) { 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( public DiagnosticSink report(
final Severity severity, final Severity severity,
final String code, final String code,
final DiagnosticPhase phase,
final String templateId,
final Map<String, String> placeholders,
final String message, final String message,
final Span span, final Span span,
final List<RelatedSpan> related) { final List<RelatedSpan> related) {
return report(new Diagnostic( return report(new Diagnostic(
Objects.requireNonNull(severity), Objects.requireNonNull(severity),
Objects.requireNonNull(code), Objects.requireNonNull(code),
Objects.requireNonNull(phase),
Objects.requireNonNull(templateId),
Map.copyOf(Objects.requireNonNull(placeholders)),
Objects.requireNonNull(message), Objects.requireNonNull(message),
Objects.requireNonNull(span), Objects.requireNonNull(span),
List.copyOf(Objects.requireNonNull(related)))); List.copyOf(Objects.requireNonNull(related))));
@ -60,16 +70,61 @@ public class DiagnosticSink implements ReadOnlyCollection<Diagnostic> {
public DiagnosticSink error( public DiagnosticSink error(
final String code, final String code,
final String templateId,
final Map<String, String> placeholders,
final String message, final String message,
final Span span) { 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( public DiagnosticSink warning(
final String code, final String code,
final String templateId,
final Map<String, String> placeholders,
final String message, final String message,
final Span span) { 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) { 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 org.junit.jupiter.api.Test;
import p.studio.compiler.source.Span; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -12,8 +15,8 @@ class DiagnosticSinkTest {
void shouldReportAndCountDiagnostics() { void shouldReportAndCountDiagnostics() {
final var sink = DiagnosticSink.empty(); final var sink = DiagnosticSink.empty();
sink.error("E001", "Unknown symbol", Span.none()); sink.error("E001", "E001", Map.of(), "Unknown symbol", Span.none());
sink.warning("W001", "Unused declaration", Span.none()); sink.warning("W001", "W001", Map.of(), "Unused declaration", Span.none());
assertEquals(2, sink.size()); assertEquals(2, sink.size());
assertTrue(sink.hasErrors()); assertTrue(sink.hasErrors());
@ -25,9 +28,9 @@ class DiagnosticSinkTest {
@Test @Test
void shouldMergeDiagnostics() { void shouldMergeDiagnostics() {
final var a = DiagnosticSink.empty() 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() final var b = DiagnosticSink.empty()
.error("E999", "Invalid type", Span.none()); .error("E999", "E999", Map.of(), "Invalid type", Span.none());
a.merge(b); a.merge(b);
@ -39,4 +42,57 @@ class DiagnosticSinkTest {
assertEquals(1, b.errorCount()); assertEquals(1, b.errorCount());
assertEquals(0, b.warningCount()); 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"));
}
} }