diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackendExecutableFunction.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackendExecutableFunction.java index d09906e8..4880d5c2 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackendExecutableFunction.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackendExecutableFunction.java @@ -23,7 +23,22 @@ public record IRBackendExecutableFunction( fileId = Objects.requireNonNull(fileId, "fileId"); moduleKey = moduleKey == null ? "" : moduleKey; callableName = Objects.requireNonNull(callableName, "callableName"); + if (callableName.isBlank()) { + throw new IllegalArgumentException("callableName must not be blank"); + } + if (sourceStart < 0 || sourceEnd < 0 || sourceEnd < sourceStart) { + throw new IllegalArgumentException("invalid source span bounds"); + } + if (paramSlots < 0 || localSlots < 0 || returnSlots < 0 || maxStackSlots < 0) { + throw new IllegalArgumentException("slots must be non-negative"); + } + if (maxStackSlots < returnSlots) { + throw new IllegalArgumentException("maxStackSlots must be >= returnSlots"); + } instructions = instructions == null ? ReadOnlyList.empty() : instructions; + for (final var instruction : instructions) { + Objects.requireNonNull(instruction, "instruction"); + } span = span == null ? Span.none() : span; } @@ -33,29 +48,62 @@ public record IRBackendExecutableFunction( String calleeCallableName, HostCallMetadata hostCall, IntrinsicCallMetadata intrinsicCall, + Integer expectedArgSlots, + Integer expectedRetSlots, Span span) { + public Instruction( + final InstructionKind kind, + final String calleeModuleKey, + final String calleeCallableName, + final HostCallMetadata hostCall, + final IntrinsicCallMetadata intrinsicCall, + final Span span) { + this(kind, calleeModuleKey, calleeCallableName, hostCall, intrinsicCall, null, null, span); + } + public Instruction { Objects.requireNonNull(kind, "kind"); span = span == null ? Span.none() : span; calleeModuleKey = calleeModuleKey == null ? "" : calleeModuleKey; calleeCallableName = calleeCallableName == null ? "" : calleeCallableName; + if (expectedArgSlots != null && expectedArgSlots < 0) { + throw new IllegalArgumentException("expectedArgSlots must be non-negative"); + } + if (expectedRetSlots != null && expectedRetSlots < 0) { + throw new IllegalArgumentException("expectedRetSlots must be non-negative"); + } switch (kind) { case CALL_FUNC -> { if (calleeCallableName.isBlank()) { throw new IllegalArgumentException("CALL_FUNC requires calleeCallableName"); } + if (hostCall != null || intrinsicCall != null) { + throw new IllegalArgumentException("CALL_FUNC must not carry host or intrinsic metadata"); + } } case CALL_HOST -> { if (hostCall == null) { throw new IllegalArgumentException("CALL_HOST requires hostCall metadata"); } + if (intrinsicCall != null) { + throw new IllegalArgumentException("CALL_HOST must not carry intrinsic metadata"); + } } case CALL_INTRINSIC -> { if (intrinsicCall == null) { throw new IllegalArgumentException("CALL_INTRINSIC requires intrinsic metadata"); } + if (hostCall != null) { + throw new IllegalArgumentException("CALL_INTRINSIC must not carry host metadata"); + } } case HALT, RET -> { + if (!calleeCallableName.isBlank() || hostCall != null || intrinsicCall != null) { + throw new IllegalArgumentException(kind + " must not carry callsite metadata"); + } + if (expectedArgSlots != null || expectedRetSlots != null) { + throw new IllegalArgumentException(kind + " must not carry expected slot metadata"); + } } } } @@ -78,6 +126,12 @@ public record IRBackendExecutableFunction( public HostCallMetadata { module = Objects.requireNonNull(module, "module"); name = Objects.requireNonNull(name, "name"); + if (module.isBlank() || name.isBlank()) { + throw new IllegalArgumentException("module and name must not be blank"); + } + if (version < 0 || argSlots < 0 || retSlots < 0) { + throw new IllegalArgumentException("host metadata values must be non-negative"); + } } } @@ -87,7 +141,12 @@ public record IRBackendExecutableFunction( int intrinsicId) { public IntrinsicCallMetadata { canonicalName = Objects.requireNonNull(canonicalName, "canonicalName"); + if (canonicalName.isBlank()) { + throw new IllegalArgumentException("canonicalName must not be blank"); + } + if (canonicalVersion < 0) { + throw new IllegalArgumentException("canonicalVersion must be non-negative"); + } } } } - diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java index b64d1c23..45b68589 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java @@ -7,6 +7,7 @@ import p.studio.utilities.structures.ReadOnlyList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class IRBackendExecutableContractTest { @@ -30,6 +31,54 @@ class IRBackendExecutableContractTest { assertEquals(IRBackendExecutableFunction.InstructionKind.CALL_FUNC, callFunc.kind()); } + @Test + void functionContractMustRejectInvalidSlotAndSpanBounds() { + assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction( + new FileId(1), + "app", + "main", + 10, + 5, + 0, + 0, + 0, + 1, + ReadOnlyList.empty(), + Span.none())); + + final var thrown = assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction( + new FileId(1), + "app", + "main", + 0, + 10, + 0, + 0, + 2, + 1, + ReadOnlyList.empty(), + Span.none())); + assertTrue(thrown.getMessage().contains("maxStackSlots")); + } + + @Test + void instructionContractMustRejectMixedMetadataKinds() { + assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.CALL_FUNC, + "app/main", + "foo", + new IRBackendExecutableFunction.HostCallMetadata("gfx", "draw", 1, 0, 0), + null, + Span.none())); + + assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction.HostCallMetadata( + "gfx", + "draw", + 1, + -1, + 0)); + } + @Test void aggregatorMustPreserveExecutableFunctionOrderDeterministically() { final var fileA = new IRBackendFile( @@ -87,4 +136,3 @@ class IRBackendExecutableContractTest { assertEquals("aux", backend.getExecutableFunctions().get(1).callableName()); } } -