implements PR007

This commit is contained in:
bQUARKz 2026-03-05 12:55:00 +00:00
parent e94f76db56
commit 62a0405353
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 854 additions and 22 deletions

View File

@ -5,17 +5,16 @@ import p.studio.compiler.models.IRBackendFile;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.pbs.semantics.PbsDeclarationSemanticsValidator;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.NameId;
import p.studio.compiler.source.tables.NameTable;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
public final class PbsFrontendCompiler {
private final PbsDeclarationSemanticsValidator declarationSemanticsValidator = new PbsDeclarationSemanticsValidator();
public IRBackendFile compileFile(
final FileId fileId,
final String source,
@ -29,26 +28,10 @@ public final class PbsFrontendCompiler {
final FileId fileId,
final PbsAst.File ast,
final DiagnosticSink diagnostics) {
validateFunctionNames(ast, diagnostics);
declarationSemanticsValidator.validate(ast, diagnostics);
return new IRBackendFile(fileId, lowerFunctions(fileId, ast));
}
private void validateFunctionNames(
final PbsAst.File ast,
final DiagnosticSink diagnostics) {
final var nameTable = new NameTable();
final Set<NameId> nameIds = new HashSet<>();
for (final var fn : ast.functions()) {
final var nameId = nameTable.register(fn.name());
if (nameIds.add(nameId)) {
continue;
}
diagnostics.error("E_RESOLVE_DUPLICATE_SYMBOL",
"Duplicate function '%s'".formatted(fn.name()),
fn.span());
}
}
private ReadOnlyList<IRFunction> lowerFunctions(final FileId fileId, final PbsAst.File ast) {
final var functions = new ArrayList<IRFunction>(ast.functions().size());
for (final var fn : ast.functions()) {

View File

@ -0,0 +1,669 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.ast.PbsAst;
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.diagnostics.Severity;
import p.studio.compiler.source.identifiers.NameId;
import p.studio.compiler.source.tables.NameTable;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class PbsDeclarationSemanticsValidator {
private final NameTable nameTable = new NameTable();
public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) {
final var binder = new NamespaceBinder(nameTable, diagnostics);
for (final var topDecl : ast.topDecls()) {
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
validateCallable(
CallableScope.topLevelFunctions(),
functionDecl.name(),
functionDecl.parameters(),
functionDecl.returnKind(),
functionDecl.returnType(),
functionDecl.resultErrorType(),
functionDecl.span(),
binder,
diagnostics);
continue;
}
if (topDecl instanceof PbsAst.StructDecl structDecl) {
binder.registerType(structDecl.name(), structDecl.span(), "struct");
validateStructDeclaration(structDecl, binder, diagnostics);
continue;
}
if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
binder.registerType(serviceDecl.name(), serviceDecl.span(), "service");
binder.registerValue(serviceDecl.name(), serviceDecl.span(), "service singleton");
validateServiceDeclaration(serviceDecl, binder, diagnostics);
continue;
}
if (topDecl instanceof PbsAst.ContractDecl contractDecl) {
binder.registerType(contractDecl.name(), contractDecl.span(), "contract");
validateContractDeclaration(contractDecl, binder, diagnostics);
continue;
}
if (topDecl instanceof PbsAst.ErrorDecl errorDecl) {
binder.registerType(errorDecl.name(), errorDecl.span(), "error");
validateErrorDeclaration(errorDecl, diagnostics);
continue;
}
if (topDecl instanceof PbsAst.EnumDecl enumDecl) {
binder.registerType(enumDecl.name(), enumDecl.span(), "enum");
validateEnumDeclaration(enumDecl, diagnostics);
continue;
}
if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
binder.registerType(callbackDecl.name(), callbackDecl.span(), "callback");
validateCallbackDeclaration(callbackDecl, diagnostics);
continue;
}
if (topDecl instanceof PbsAst.ConstDecl constDecl) {
binder.registerValue(constDecl.name(), constDecl.span(), "const");
validateConstDeclaration(constDecl, diagnostics);
continue;
}
if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) {
validateImplementsDeclaration(implementsDecl, binder, diagnostics);
}
}
}
private void validateStructDeclaration(
final PbsAst.StructDecl structDecl,
final NamespaceBinder binder,
final DiagnosticSink diagnostics) {
final var methodScope = CallableScope.structMethods(nameTable.register(structDecl.name()));
for (final var method : structDecl.methods()) {
validateCallable(
methodScope,
method.name(),
method.parameters(),
method.returnKind(),
method.returnType(),
method.resultErrorType(),
method.span(),
binder,
diagnostics);
}
final var ctorScope = CallableScope.structCtors(nameTable.register(structDecl.name()));
for (final var ctor : structDecl.ctors()) {
validateParameters(ctor.parameters(), "ctor '%s'".formatted(ctor.name()), diagnostics);
validateTypeSurfaceRecursive(
ctor.parameters().stream().map(PbsAst.Parameter::typeRef).toList(),
"ctor '%s'".formatted(ctor.name()),
diagnostics);
binder.registerCtor(ctorScope, ctor.name(), ctorShapeKey(ctor.parameters()), ctor.span());
if (containsReturnStatement(ctor.body())) {
diagnostics.error(
PbsSemanticsErrors.E_SEM_INVALID_RETURN_INSIDE_CTOR.name(),
"Constructors cannot contain 'return' statements",
ctor.span());
}
}
}
private void validateServiceDeclaration(
final PbsAst.ServiceDecl serviceDecl,
final NamespaceBinder binder,
final DiagnosticSink diagnostics) {
final var methodScope = CallableScope.serviceMethods(nameTable.register(serviceDecl.name()));
for (final var method : serviceDecl.methods()) {
validateCallable(
methodScope,
method.name(),
method.parameters(),
method.returnKind(),
method.returnType(),
method.resultErrorType(),
method.span(),
binder,
diagnostics);
}
}
private void validateContractDeclaration(
final PbsAst.ContractDecl contractDecl,
final NamespaceBinder binder,
final DiagnosticSink diagnostics) {
final var signatureScope = CallableScope.contractSignatures(nameTable.register(contractDecl.name()));
for (final var signature : contractDecl.signatures()) {
validateCallable(
signatureScope,
signature.name(),
signature.parameters(),
signature.returnKind(),
signature.returnType(),
signature.resultErrorType(),
signature.span(),
binder,
diagnostics);
}
}
private void validateErrorDeclaration(
final PbsAst.ErrorDecl errorDecl,
final DiagnosticSink diagnostics) {
final var seenCases = new HashMap<NameId, Span>();
for (final var caseName : errorDecl.cases()) {
final var caseNameId = nameTable.register(caseName);
final var first = seenCases.putIfAbsent(caseNameId, errorDecl.span());
if (first != null) {
diagnostics.report(
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_ERROR_CASE_LABEL.name(),
"Duplicate error case label '%s' in '%s'".formatted(caseName, errorDecl.name()),
errorDecl.span(),
List.of(new RelatedSpan("First case is here", first)));
}
}
}
private void validateEnumDeclaration(
final PbsAst.EnumDecl enumDecl,
final DiagnosticSink diagnostics) {
final var seenLabels = new HashMap<NameId, Span>();
final var seenIds = new HashMap<Long, Span>();
for (final var enumCase : enumDecl.cases()) {
final var labelId = nameTable.register(enumCase.name());
final var firstLabel = seenLabels.putIfAbsent(labelId, enumCase.span());
if (firstLabel != null) {
diagnostics.report(
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_LABEL.name(),
"Duplicate enum case label '%s' in '%s'".formatted(enumCase.name(), enumDecl.name()),
enumCase.span(),
List.of(new RelatedSpan("First case is here", firstLabel)));
}
if (enumCase.explicitValue() != null) {
final var firstId = seenIds.putIfAbsent(enumCase.explicitValue(), enumCase.span());
if (firstId != null) {
diagnostics.report(
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_ID.name(),
"Duplicate enum case id '%s' in '%s'".formatted(enumCase.explicitValue(), enumDecl.name()),
enumCase.span(),
List.of(new RelatedSpan("First id is here", firstId)));
}
}
}
}
private void validateCallbackDeclaration(
final PbsAst.CallbackDecl callbackDecl,
final DiagnosticSink diagnostics) {
validateParameters(callbackDecl.parameters(), "callback '%s'".formatted(callbackDecl.name()), diagnostics);
validateReturnSurface(
callbackDecl.returnKind(),
callbackDecl.returnType(),
callbackDecl.resultErrorType(),
"callback '%s'".formatted(callbackDecl.name()),
callbackDecl.span(),
diagnostics);
validateTypeSurfaceRecursive(
callbackDecl.parameters().stream().map(PbsAst.Parameter::typeRef).toList(),
"callback '%s'".formatted(callbackDecl.name()),
diagnostics);
}
private void validateConstDeclaration(
final PbsAst.ConstDecl constDecl,
final DiagnosticSink diagnostics) {
if (constDecl.explicitType() == null) {
diagnostics.error(
PbsSemanticsErrors.E_SEM_MISSING_CONST_TYPE_ANNOTATION.name(),
"Const declaration '%s' must include an explicit type annotation".formatted(constDecl.name()),
constDecl.span());
} else {
validateTypeSurfaceRecursive(
List.of(constDecl.explicitType()),
"const '%s'".formatted(constDecl.name()),
diagnostics);
}
if (constDecl.initializer() == null) {
diagnostics.error(
PbsSemanticsErrors.E_SEM_MISSING_CONST_INITIALIZER.name(),
"Non-builtin const declaration '%s' must include an initializer".formatted(constDecl.name()),
constDecl.span());
}
}
private void validateImplementsDeclaration(
final PbsAst.ImplementsDecl implementsDecl,
final NamespaceBinder binder,
final DiagnosticSink diagnostics) {
final var ownerNameId = nameTable.register(implementsDecl.ownerName() + ":" + implementsDecl.contractName());
final var scope = CallableScope.implementsMethods(ownerNameId);
for (final var method : implementsDecl.methods()) {
validateCallable(
scope,
method.name(),
method.parameters(),
method.returnKind(),
method.returnType(),
method.resultErrorType(),
method.span(),
binder,
diagnostics);
}
}
private void validateCallable(
final CallableScope scope,
final String callableName,
final ReadOnlyList<PbsAst.Parameter> parameters,
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType,
final PbsAst.TypeRef resultErrorType,
final Span span,
final NamespaceBinder binder,
final DiagnosticSink diagnostics) {
validateParameters(parameters, "callable '%s'".formatted(callableName), diagnostics);
validateReturnSurface(returnKind, returnType, resultErrorType, "callable '%s'".formatted(callableName), span, diagnostics);
validateTypeSurfaceRecursive(parameters.stream().map(PbsAst.Parameter::typeRef).toList(), "callable '%s'".formatted(callableName), diagnostics);
binder.registerCallable(scope, callableName, callableShapeKey(parameters, returnKind, returnType, resultErrorType), span);
}
private void validateParameters(
final ReadOnlyList<PbsAst.Parameter> parameters,
final String ownerDescription,
final DiagnosticSink diagnostics) {
final var seen = new HashMap<NameId, Span>();
for (final var parameter : parameters) {
final var parameterNameId = nameTable.register(parameter.name());
final var first = seen.putIfAbsent(parameterNameId, parameter.span());
if (first != null) {
diagnostics.report(
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_PARAMETER_NAME.name(),
"Duplicate parameter name '%s' in %s".formatted(parameter.name(), ownerDescription),
parameter.span(),
List.of(new RelatedSpan("First parameter is here", first)));
}
}
}
private void validateReturnSurface(
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType,
final PbsAst.TypeRef resultErrorType,
final String ownerDescription,
final Span span,
final DiagnosticSink diagnostics) {
if (returnKind == PbsAst.ReturnKind.RESULT && isOptionalType(returnType)) {
diagnostics.error(
PbsSemanticsErrors.E_SEM_INVALID_MIXED_OPTIONAL_RESULT_RETURN.name(),
"Return surface for %s cannot combine 'result' with top-level 'optional' payload".formatted(ownerDescription),
span);
}
if (returnType != null && returnType.kind() == PbsAst.TypeRefKind.NAMED_TUPLE) {
validateDuplicateOutputLabels(returnType, ownerDescription, diagnostics);
}
if (returnType != null) {
validateTypeSurfaceRecursive(List.of(returnType), ownerDescription, diagnostics);
}
if (resultErrorType != null) {
validateTypeSurfaceRecursive(List.of(resultErrorType), ownerDescription, diagnostics);
}
}
private void validateDuplicateOutputLabels(
final PbsAst.TypeRef namedTupleReturnType,
final String ownerDescription,
final DiagnosticSink diagnostics) {
final var seen = new HashMap<NameId, Span>();
for (final var field : namedTupleReturnType.fields()) {
final var labelId = nameTable.register(field.label());
final var first = seen.putIfAbsent(labelId, field.span());
if (first != null) {
diagnostics.report(
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_RETURN_LABEL.name(),
"Duplicate return label '%s' in %s".formatted(field.label(), ownerDescription),
field.span(),
List.of(new RelatedSpan("First label is here", first)));
}
}
}
private void validateTypeSurfaceRecursive(
final List<PbsAst.TypeRef> roots,
final String ownerDescription,
final DiagnosticSink diagnostics) {
final var pending = new ArrayList<>(roots);
while (!pending.isEmpty()) {
final var typeRef = pending.removeLast();
if (typeRef == null) {
continue;
}
switch (typeRef.kind()) {
case OPTIONAL -> {
if (typeRef.inner() == null || typeRef.inner().kind() == PbsAst.TypeRefKind.UNIT) {
diagnostics.error(
PbsSemanticsErrors.E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE.name(),
"Invalid optional type surface in %s: 'optional void' is not allowed".formatted(ownerDescription),
typeRef.span());
}
pending.add(typeRef.inner());
}
case GROUP -> pending.add(typeRef.inner());
case NAMED_TUPLE -> {
for (final var field : typeRef.fields()) {
pending.add(field.typeRef());
}
}
default -> {
// No recursive children.
}
}
}
}
private boolean isOptionalType(final PbsAst.TypeRef typeRef) {
if (typeRef == null) {
return false;
}
if (typeRef.kind() == PbsAst.TypeRefKind.OPTIONAL) {
return true;
}
if (typeRef.kind() == PbsAst.TypeRefKind.GROUP) {
return isOptionalType(typeRef.inner());
}
return false;
}
private boolean containsReturnStatement(final PbsAst.Block block) {
if (block == null) {
return false;
}
for (final var statement : block.statements()) {
if (statement instanceof PbsAst.ReturnStatement) {
return true;
}
if (statement instanceof PbsAst.IfStatement ifStatement) {
if (containsReturnInIfStatement(ifStatement)) {
return true;
}
continue;
}
if (statement instanceof PbsAst.ForStatement forStatement && containsReturnStatement(forStatement.body())) {
return true;
}
if (statement instanceof PbsAst.WhileStatement whileStatement && containsReturnStatement(whileStatement.body())) {
return true;
}
if (statement instanceof PbsAst.ExpressionStatement expressionStatement && containsReturnExpression(expressionStatement.expression())) {
return true;
}
}
return block.tailExpression() != null && containsReturnExpression(block.tailExpression());
}
private boolean containsReturnInIfStatement(final PbsAst.IfStatement ifStatement) {
if (ifStatement == null) {
return false;
}
if (containsReturnStatement(ifStatement.thenBlock()) || containsReturnStatement(ifStatement.elseBlock())) {
return true;
}
return containsReturnInIfStatement(ifStatement.elseIf());
}
private boolean containsReturnExpression(final PbsAst.Expression expression) {
if (expression == null) {
return false;
}
if (expression instanceof PbsAst.BlockExpr blockExpr) {
return containsReturnStatement(blockExpr.block());
}
if (expression instanceof PbsAst.IfExpr ifExpr) {
return containsReturnStatement(ifExpr.thenBlock()) || containsReturnExpression(ifExpr.elseExpression());
}
if (expression instanceof PbsAst.SwitchExpr switchExpr) {
for (final var arm : switchExpr.arms()) {
if (containsReturnStatement(arm.block())) {
return true;
}
}
}
if (expression instanceof PbsAst.HandleExpr handleExpr) {
for (final var arm : handleExpr.arms()) {
if (containsReturnStatement(arm.block())) {
return true;
}
}
}
return false;
}
private String callableShapeKey(
final ReadOnlyList<PbsAst.Parameter> parameters,
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType,
final PbsAst.TypeRef resultErrorType) {
final var builder = new StringBuilder();
builder.append('(');
for (int i = 0; i < parameters.size(); i++) {
if (i > 0) {
builder.append(',');
}
builder.append(typeKey(parameters.get(i).typeRef()));
}
builder.append(")->");
builder.append(outputSurfaceKey(returnKind, returnType, resultErrorType));
return builder.toString();
}
private String ctorShapeKey(final ReadOnlyList<PbsAst.Parameter> parameters) {
final var builder = new StringBuilder();
builder.append('(');
for (int i = 0; i < parameters.size(); i++) {
if (i > 0) {
builder.append(',');
}
builder.append(typeKey(parameters.get(i).typeRef()));
}
builder.append(')');
return builder.toString();
}
private String outputSurfaceKey(
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType,
final PbsAst.TypeRef resultErrorType) {
return switch (returnKind) {
case INFERRED_UNIT, EXPLICIT_UNIT -> "unit";
case PLAIN -> carrierTypeKey(returnType);
case RESULT -> "result<" + typeKey(resultErrorType) + ">" + carrierTypeKey(returnType);
};
}
private String carrierTypeKey(final PbsAst.TypeRef typeRef) {
final var unwrapped = unwrapGroup(typeRef);
if (unwrapped != null && unwrapped.kind() == PbsAst.TypeRefKind.NAMED_TUPLE && unwrapped.fields().size() == 1) {
return typeKey(unwrapped.fields().getFirst().typeRef());
}
return typeKey(unwrapped);
}
private PbsAst.TypeRef unwrapGroup(final PbsAst.TypeRef typeRef) {
if (typeRef == null) {
return null;
}
if (typeRef.kind() != PbsAst.TypeRefKind.GROUP) {
return typeRef;
}
return unwrapGroup(typeRef.inner());
}
private String typeKey(final PbsAst.TypeRef typeRef) {
if (typeRef == null) {
return "unit";
}
final var unwrapped = unwrapGroup(typeRef);
if (unwrapped == null) {
return "unit";
}
return switch (unwrapped.kind()) {
case SIMPLE -> "simple:" + unwrapped.name();
case SELF -> "self";
case OPTIONAL -> "optional(" + typeKey(unwrapped.inner()) + ")";
case UNIT -> "unit";
case GROUP -> typeKey(unwrapped.inner());
case NAMED_TUPLE -> "tuple(" + unwrapped.fields().stream()
.map(field -> typeKey(field.typeRef()))
.reduce((left, right) -> left + "," + right)
.orElse("") + ")";
case ERROR -> "error";
};
}
private static final class NamespaceBinder {
private final NameTable nameTable;
private final DiagnosticSink diagnostics;
private final Map<NameId, Span> typeNamespace = new HashMap<>();
private final Map<NameId, Span> valueNamespace = new HashMap<>();
private final Map<NameId, Span> hostOwnerNamespace = new HashMap<>();
private final Map<CallableIdentity, Span> callableNamespace = new HashMap<>();
private NamespaceBinder(final NameTable nameTable, final DiagnosticSink diagnostics) {
this.nameTable = nameTable;
this.diagnostics = diagnostics;
}
private void registerType(final String name, final Span span, final String kind) {
registerByNamespace(typeNamespace, name, span, "type namespace", kind);
}
private void registerValue(final String name, final Span span, final String kind) {
registerByNamespace(valueNamespace, name, span, "value namespace", kind);
}
@SuppressWarnings("unused")
private void registerHostOwner(final String name, final Span span, final String kind) {
registerByNamespace(hostOwnerNamespace, name, span, "host-owner namespace", kind);
}
private void registerCallable(
final CallableScope scope,
final String callableName,
final String shape,
final Span span) {
final var callableNameId = nameTable.register(callableName);
final var identity = new CallableIdentity(scope, callableNameId, shape);
final var first = callableNamespace.putIfAbsent(identity, span);
if (first == null) {
return;
}
diagnostics.report(
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name(),
"Duplicate callable declaration '%s' with shape %s in %s".formatted(callableName, shape, scope.label()),
span,
List.of(new RelatedSpan("First callable declaration is here", first)));
}
private void registerCtor(
final CallableScope scope,
final String ctorName,
final String shape,
final Span span) {
final var callableNameId = nameTable.register(ctorName);
final var identity = new CallableIdentity(scope, callableNameId, shape);
final var first = callableNamespace.putIfAbsent(identity, span);
if (first == null) {
return;
}
diagnostics.report(
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name(),
"Duplicate constructor declaration '%s' with shape %s in %s".formatted(ctorName, shape, scope.label()),
span,
List.of(new RelatedSpan("First constructor declaration is here", first)));
}
private void registerByNamespace(
final Map<NameId, Span> namespace,
final String declarationName,
final Span span,
final String namespaceName,
final String declarationKind) {
final var nameId = nameTable.register(declarationName);
final var first = namespace.putIfAbsent(nameId, span);
if (first == null) {
return;
}
diagnostics.report(
Severity.Error,
PbsSemanticsErrors.E_SEM_DUPLICATE_DECLARATION.name(),
"Duplicate %s declaration '%s' in %s".formatted(declarationKind, declarationName, namespaceName),
span,
List.of(new RelatedSpan("First declaration is here", first)));
}
}
private record CallableIdentity(
CallableScope scope,
NameId callableNameId,
String shape) {
}
private record CallableScope(
String label,
String scopeKind,
NameId ownerNameId) {
private static CallableScope topLevelFunctions() {
return new CallableScope("top-level callable scope", "top-level", null);
}
private static CallableScope structMethods(final NameId ownerNameId) {
return new CallableScope("struct method scope", "struct", ownerNameId);
}
private static CallableScope structCtors(final NameId ownerNameId) {
return new CallableScope("struct constructor scope", "ctor", ownerNameId);
}
private static CallableScope serviceMethods(final NameId ownerNameId) {
return new CallableScope("service method scope", "service", ownerNameId);
}
private static CallableScope contractSignatures(final NameId ownerNameId) {
return new CallableScope("contract signature scope", "contract", ownerNameId);
}
private static CallableScope implementsMethods(final NameId ownerNameId) {
return new CallableScope("implements method scope", "implements", ownerNameId);
}
}
}

View File

@ -0,0 +1,16 @@
package p.studio.compiler.pbs.semantics;
public enum PbsSemanticsErrors {
E_SEM_DUPLICATE_DECLARATION,
E_SEM_DUPLICATE_CALLABLE_SHAPE,
E_SEM_DUPLICATE_PARAMETER_NAME,
E_SEM_DUPLICATE_RETURN_LABEL,
E_SEM_DUPLICATE_ERROR_CASE_LABEL,
E_SEM_DUPLICATE_ENUM_CASE_LABEL,
E_SEM_DUPLICATE_ENUM_CASE_ID,
E_SEM_INVALID_MIXED_OPTIONAL_RESULT_RETURN,
E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE,
E_SEM_MISSING_CONST_TYPE_ANNOTATION,
E_SEM_MISSING_CONST_INITIALIZER,
E_SEM_INVALID_RETURN_INSIDE_CTOR,
}

View File

@ -1,6 +1,7 @@
package p.studio.compiler.pbs;
import org.junit.jupiter.api.Test;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
@ -45,6 +46,22 @@ class PbsFrontendCompilerTest {
final var compiler = new PbsFrontendCompiler();
compiler.compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.hasErrors());
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
}
@Test
void shouldAllowOverloadedFunctionNamesWhenShapeDiffers() {
final var source = """
fn a(x: int) -> int { return x; }
fn a(x: float) -> int { return 0; }
""";
final var diagnostics = DiagnosticSink.empty();
final var compiler = new PbsFrontendCompiler();
compiler.compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().noneMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
}
}

View File

@ -0,0 +1,147 @@
package p.studio.compiler.pbs.semantics;
import org.junit.jupiter.api.Test;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsSemanticsDeclarationsTest {
@Test
void shouldAllowFunctionOverloadsWithDifferentShapes() {
final var source = """
fn run(x: int) -> int { return x; }
fn run(x: float) -> int { return 0; }
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertFalse(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
}
@Test
void shouldRejectDuplicateCallableShapeIgnoringLabels() {
final var source = """
fn run(a: int) -> (x: int, y: int) { return 0; }
fn run(b: int) -> (left: int, right: int) { return 1; }
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
}
@Test
void shouldRejectDuplicateParametersAndReturnLabels() {
final var source = """
fn dup(a: int, a: int) -> (x: int, x: int) { return 0; }
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_PARAMETER_NAME.name())));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RETURN_LABEL.name())));
}
@Test
void shouldRejectDuplicateEnumAndErrorCases() {
final var source = """
declare error Err { Fail; Fail; }
declare enum Mode(Idle = 0, Idle = 1, Run = 1);
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_ERROR_CASE_LABEL.name())));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_LABEL.name())));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_ID.name())));
}
@Test
void shouldRejectInvalidReturnSurfaces() {
final var source = """
declare error Err { Fail; }
fn badResult() -> result<Err> optional int { return ok(1); }
fn badOptional(v: optional void) -> int { return 0; }
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_MIXED_OPTIONAL_RESULT_RETURN.name())));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE.name())));
}
@Test
void shouldRejectConstWithoutInitializer() {
final var source = "declare const LIMIT: int;";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_MISSING_CONST_INITIALIZER.name())));
}
@Test
void shouldRejectInvalidCtorServiceAndCallbackShapes() {
final var source = """
declare struct S(a: int) {
ctor make(x: int) { return; }
ctor make(y: int) { }
}
declare service Game {
fn run(a: int) -> int { return a; }
fn run(b: int) -> int { return b; }
}
declare callback Tick(a: int, a: int) -> int;
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_RETURN_INSIDE_CTOR.name())));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name())));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_PARAMETER_NAME.name())));
}
@Test
void shouldRejectDuplicateDeclarationNamesByNamespace() {
final var source = """
declare struct State();
declare enum State(Idle);
declare const Tick: int = 1;
declare service Tick {
fn run() -> void { return; }
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_DECLARATION.name())));
}
}