diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeFunctionLayoutBuilder.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeFunctionLayoutBuilder.java new file mode 100644 index 00000000..55606434 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeFunctionLayoutBuilder.java @@ -0,0 +1,93 @@ +package p.studio.compiler.backend.bytecode; + +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; +import java.util.Objects; + +public final class BytecodeFunctionLayoutBuilder { + private BytecodeFunctionLayoutBuilder() { + } + + public static LayoutResult build( + final ReadOnlyList functions) { + final var inputFunctions = functions == null ? ReadOnlyList.empty() : functions; + final var metas = new ArrayList(inputFunctions.size()); + final var names = new ArrayList(inputFunctions.size()); + final var spans = new ArrayList(); + var totalCodeLen = 0; + for (var i = 0; i < inputFunctions.size(); i++) { + final var fn = inputFunctions.get(i); + final var fnCode = fn.code(); + metas.add(new BytecodeModule.FunctionMeta( + totalCodeLen, + fnCode.length, + fn.paramSlots(), + fn.localSlots(), + fn.returnSlots(), + fn.maxStackSlots())); + names.add(new BytecodeModule.FunctionName(i, fn.name())); + + for (final var span : fn.instructionSpans()) { + if (span.functionPc() < 0 || span.functionPc() >= fnCode.length) { + throw new BytecodeMarshalingException( + BytecodeMarshalingErrorCode.MARSHAL_FORMAT_PC_SPAN_OUT_OF_BOUNDS, + "pc_to_span offset is out of bounds for function: " + fn.name()); + } + spans.add(new BytecodeModule.PcToSpan( + totalCodeLen + span.functionPc(), + span.span())); + } + totalCodeLen += fnCode.length; + } + + final var flattened = new byte[totalCodeLen]; + var cursor = 0; + for (final var fn : inputFunctions) { + final var fnCode = fn.code(); + System.arraycopy(fnCode, 0, flattened, cursor, fnCode.length); + cursor += fnCode.length; + } + + return new LayoutResult( + flattened, + ReadOnlyList.wrap(metas), + new BytecodeModule.DebugInfo( + ReadOnlyList.wrap(spans), + ReadOnlyList.wrap(names))); + } + + public record FunctionFragment( + String name, + byte[] code, + int paramSlots, + int localSlots, + int returnSlots, + int maxStackSlots, + ReadOnlyList instructionSpans) { + public FunctionFragment { + Objects.requireNonNull(name, "name"); + code = code == null ? new byte[0] : code.clone(); + instructionSpans = instructionSpans == null ? ReadOnlyList.empty() : instructionSpans; + } + } + + public record InstructionSpan( + int functionPc, + BytecodeModule.SourceSpan span) { + public InstructionSpan { + Objects.requireNonNull(span, "span"); + } + } + + public record LayoutResult( + byte[] code, + ReadOnlyList functions, + BytecodeModule.DebugInfo debugInfo) { + public LayoutResult { + code = code == null ? new byte[0] : code.clone(); + functions = functions == null ? ReadOnlyList.empty() : functions; + } + } +} + diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java index c5449ecd..23999e6c 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeMarshalingErrorCode.java @@ -4,5 +4,5 @@ public enum BytecodeMarshalingErrorCode { MARSHAL_FORMAT_STRING_TOO_LONG, MARSHAL_FORMAT_SYSC_MODULE_TOO_LONG, MARSHAL_FORMAT_SYSC_NAME_TOO_LONG, + MARSHAL_FORMAT_PC_SPAN_OUT_OF_BOUNDS, } - diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeFunctionLayoutBuilderTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeFunctionLayoutBuilderTest.java new file mode 100644 index 00000000..ec25bffe --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/bytecode/BytecodeFunctionLayoutBuilderTest.java @@ -0,0 +1,82 @@ +package p.studio.compiler.backend.bytecode; + +import org.junit.jupiter.api.Test; +import p.studio.utilities.structures.ReadOnlyList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BytecodeFunctionLayoutBuilderTest { + + @Test + void buildMustProduceMonotonicOffsets() { + final var layout = BytecodeFunctionLayoutBuilder.build(ReadOnlyList.from( + new BytecodeFunctionLayoutBuilder.FunctionFragment( + "main", + new byte[] { 1, 2, 3 }, + 1, + 2, + 0, + 4, + ReadOnlyList.empty()), + new BytecodeFunctionLayoutBuilder.FunctionFragment( + "aux", + new byte[] { 4, 5 }, + 0, + 1, + 1, + 2, + ReadOnlyList.empty()))); + + assertEquals(2, layout.functions().size()); + assertEquals(0, layout.functions().get(0).codeOffset()); + assertEquals(3, layout.functions().get(0).codeLen()); + assertEquals(3, layout.functions().get(1).codeOffset()); + assertEquals(2, layout.functions().get(1).codeLen()); + assertEquals(5, layout.code().length); + } + + @Test + void buildMustEmitDebugMinimum() { + final var layout = BytecodeFunctionLayoutBuilder.build(ReadOnlyList.from( + new BytecodeFunctionLayoutBuilder.FunctionFragment( + "main", + new byte[] { 8, 9 }, + 0, + 0, + 0, + 1, + ReadOnlyList.from( + new BytecodeFunctionLayoutBuilder.InstructionSpan( + 0, + new BytecodeModule.SourceSpan(10, 0, 1)), + new BytecodeFunctionLayoutBuilder.InstructionSpan( + 1, + new BytecodeModule.SourceSpan(10, 2, 3)))))); + + assertEquals(1, layout.debugInfo().functionNames().size()); + assertEquals("main", layout.debugInfo().functionNames().getFirst().name()); + assertEquals(2, layout.debugInfo().pcToSpan().size()); + assertEquals(0, layout.debugInfo().pcToSpan().get(0).pc()); + assertEquals(1, layout.debugInfo().pcToSpan().get(1).pc()); + } + + @Test + void buildMustRejectOutOfBoundsDebugPc() { + final var thrown = assertThrows(BytecodeMarshalingException.class, () -> BytecodeFunctionLayoutBuilder.build(ReadOnlyList.from( + new BytecodeFunctionLayoutBuilder.FunctionFragment( + "main", + new byte[] { 1 }, + 0, + 0, + 0, + 1, + ReadOnlyList.from( + new BytecodeFunctionLayoutBuilder.InstructionSpan( + 2, + new BytecodeModule.SourceSpan(1, 0, 1))))))); + + assertEquals(BytecodeMarshalingErrorCode.MARSHAL_FORMAT_PC_SPAN_OUT_OF_BOUNDS, thrown.code()); + } +} +