diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistry.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistry.java index 47337d0f..807189db 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistry.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistry.java @@ -1,11 +1,16 @@ package p.studio.compiler.backend.irvm; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.OptionalInt; final class IRVMIntrinsicRegistry { - private static final Map FINAL_ID_BY_INTRINSIC = createRegistry(); + private static final String REGISTRY_RESOURCE = "/intrinsics/registry-v1.csv"; + private static final Map FINAL_ID_BY_INTRINSIC = loadRegistry(); private IRVMIntrinsicRegistry() { } @@ -23,38 +28,79 @@ final class IRVMIntrinsicRegistry { return OptionalInt.of(resolved); } - private static Map createRegistry() { + static Map snapshotByCanonicalIdentity() { + final var snapshot = new HashMap(FINAL_ID_BY_INTRINSIC.size()); + for (final var entry : FINAL_ID_BY_INTRINSIC.entrySet()) { + snapshot.put(canonicalIdentity(entry.getKey().canonicalName(), entry.getKey().canonicalVersion()), entry.getValue()); + } + return Map.copyOf(snapshot); + } + + private static Map loadRegistry() { + final var stream = IRVMIntrinsicRegistry.class.getResourceAsStream(REGISTRY_RESOURCE); + if (stream == null) { + throw new IllegalStateException("missing intrinsic registry resource: " + REGISTRY_RESOURCE); + } + final var registry = new HashMap(); - register(registry, "vec2.dot", 1, 0x1000); - register(registry, "vec2.length", 1, 0x1001); - - register(registry, "input.pad", 1, 0x2000); - register(registry, "input.touch", 1, 0x2001); - - register(registry, "input.pad.up", 1, 0x2010); - register(registry, "input.pad.down", 1, 0x2011); - register(registry, "input.pad.left", 1, 0x2012); - register(registry, "input.pad.right", 1, 0x2013); - register(registry, "input.pad.a", 1, 0x2014); - register(registry, "input.pad.b", 1, 0x2015); - register(registry, "input.pad.x", 1, 0x2016); - register(registry, "input.pad.y", 1, 0x2017); - register(registry, "input.pad.l", 1, 0x2018); - register(registry, "input.pad.r", 1, 0x2019); - register(registry, "input.pad.start", 1, 0x201A); - register(registry, "input.pad.select", 1, 0x201B); - - register(registry, "input.touch.button", 1, 0x2020); - register(registry, "input.touch.x", 1, 0x2021); - register(registry, "input.touch.y", 1, 0x2022); - - register(registry, "input.button.pressed", 1, 0x2030); - register(registry, "input.button.released", 1, 0x2031); - register(registry, "input.button.down", 1, 0x2032); - register(registry, "input.button.hold", 1, 0x2033); + final var identityByFinalId = new HashMap(); + try (final var reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + String rawLine; + int lineNumber = 0; + while ((rawLine = reader.readLine()) != null) { + lineNumber++; + final var line = rawLine.trim(); + if (line.isBlank() || line.startsWith("#")) { + continue; + } + final var columns = line.split(",", -1); + if (columns.length != 3) { + throw new IllegalStateException("invalid intrinsic registry row at line " + lineNumber + ": " + line); + } + final var canonicalName = columns[0].trim(); + final long canonicalVersion; + try { + canonicalVersion = Long.parseLong(columns[1].trim()); + } catch (NumberFormatException ex) { + throw new IllegalStateException("invalid intrinsic version at line " + lineNumber + ": " + line, ex); + } + final int finalId = parseFinalId(columns[2].trim(), lineNumber, line); + register(registry, canonicalName, canonicalVersion, finalId); + final var identity = canonicalIdentity(canonicalName, canonicalVersion); + final var previous = identityByFinalId.putIfAbsent(finalId, identity); + if (previous != null && !previous.equals(identity)) { + throw new IllegalStateException( + "duplicate intrinsic final id 0x%08X for %s and %s".formatted(finalId, previous, identity)); + } + } + } catch (IOException ex) { + throw new IllegalStateException("failed to read intrinsic registry resource: " + REGISTRY_RESOURCE, ex); + } return Map.copyOf(registry); } + private static int parseFinalId( + final String rawId, + final int lineNumber, + final String line) { + try { + if (rawId.startsWith("0x") || rawId.startsWith("0X")) { + return Integer.parseUnsignedInt(rawId.substring(2), 16); + } + return Integer.parseUnsignedInt(rawId, 10); + } catch (NumberFormatException ex) { + throw new IllegalStateException( + "invalid intrinsic final id at line %d: %s".formatted(lineNumber, line), + ex); + } + } + + private static String canonicalIdentity( + final String canonicalName, + final long canonicalVersion) { + return canonicalName + "@" + canonicalVersion; + } + private static void register( final Map registry, final String canonicalName, diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/resources/intrinsics/registry-v1.csv b/prometeu-compiler/prometeu-build-pipeline/src/main/resources/intrinsics/registry-v1.csv new file mode 100644 index 00000000..1f1da908 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/resources/intrinsics/registry-v1.csv @@ -0,0 +1,24 @@ +# canonicalName,canonicalVersion,finalId +vec2.dot,1,0x1000 +vec2.length,1,0x1001 +input.pad,1,0x2000 +input.touch,1,0x2001 +input.pad.up,1,0x2010 +input.pad.down,1,0x2011 +input.pad.left,1,0x2012 +input.pad.right,1,0x2013 +input.pad.a,1,0x2014 +input.pad.b,1,0x2015 +input.pad.x,1,0x2016 +input.pad.y,1,0x2017 +input.pad.l,1,0x2018 +input.pad.r,1,0x2019 +input.pad.start,1,0x201A +input.pad.select,1,0x201B +input.touch.button,1,0x2020 +input.touch.x,1,0x2021 +input.touch.y,1,0x2022 +input.button.pressed,1,0x2030 +input.button.released,1,0x2031 +input.button.down,1,0x2032 +input.button.hold,1,0x2033 diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistryParityTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistryParityTest.java new file mode 100644 index 00000000..021f9f56 --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistryParityTest.java @@ -0,0 +1,163 @@ +package p.studio.compiler.backend.irvm; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; + +class IRVMIntrinsicRegistryParityTest { + private static final Path RUNTIME_INTRINSICS_FILE = Path.of("../runtime/crates/console/prometeu-vm/src/builtins.rs"); + private static final Pattern OWNER_PATTERN = Pattern.compile("owner:\\s*\"([^\"]+)\","); + private static final Pattern NAME_PATTERN = Pattern.compile("name:\\s*\"([^\"]+)\","); + private static final Pattern VERSION_PATTERN = Pattern.compile("version:\\s*(\\d+),"); + private static final Pattern ID_PATTERN = Pattern.compile("id:\\s*(0x[0-9A-Fa-f]+|\\d+),"); + private static final String STRICT_PARITY_ENV = "PROMETEU_INTRINSIC_PARITY_STRICT"; + + @Test + void registryResourceMustExposeExpectedCanonicalEntries() { + final var snapshot = IRVMIntrinsicRegistry.snapshotByCanonicalIdentity(); + assertFalse(snapshot.isEmpty()); + assertEquals(23, snapshot.size()); + assertEquals(0x2000, snapshot.get("input.pad@1")); + assertEquals(0x2033, snapshot.get("input.button.hold@1")); + } + + @Test + void registryMustStayInSyncWithRuntimeBuiltinsTable() throws IOException { + final var compilerSnapshot = IRVMIntrinsicRegistry.snapshotByCanonicalIdentity(); + if (!Files.exists(RUNTIME_INTRINSICS_FILE)) { + if (strictParityEnabled()) { + fail("runtime intrinsic table not found: " + RUNTIME_INTRINSICS_FILE.toAbsolutePath()); + } + return; + } + final var runtimeSnapshot = parseRuntimeIntrinsicSnapshot(RUNTIME_INTRINSICS_FILE); + assertEquals(runtimeSnapshot, compilerSnapshot, parityDiff(runtimeSnapshot, compilerSnapshot)); + } + + private Map parseRuntimeIntrinsicSnapshot(final Path runtimeFile) throws IOException { + final var runtimeSource = Files.readAllLines(runtimeFile, StandardCharsets.UTF_8); + final var mapped = new LinkedHashMap(); + var insideIntrinsicsArray = false; + var insideEntry = false; + String owner = null; + String name = null; + Long version = null; + Integer id = null; + for (final var rawLine : runtimeSource) { + final var line = rawLine.trim(); + if (!insideIntrinsicsArray) { + if (line.startsWith("const INTRINSICS:")) { + insideIntrinsicsArray = true; + } + continue; + } + if (line.equals("];")) { + break; + } + if (line.startsWith("IntrinsicMeta {")) { + insideEntry = true; + owner = null; + name = null; + version = null; + id = null; + continue; + } + if (!insideEntry) { + continue; + } + final var ownerMatcher = OWNER_PATTERN.matcher(line); + if (ownerMatcher.find()) { + owner = ownerMatcher.group(1); + } + final var nameMatcher = NAME_PATTERN.matcher(line); + if (nameMatcher.find()) { + name = nameMatcher.group(1); + } + final var versionMatcher = VERSION_PATTERN.matcher(line); + if (versionMatcher.find()) { + version = Long.parseLong(versionMatcher.group(1)); + } + final var idMatcher = ID_PATTERN.matcher(line); + if (idMatcher.find()) { + id = parseRuntimeId(idMatcher.group(1)); + } + if (line.equals("},")) { + if (owner == null || name == null || version == null || id == null) { + throw new IllegalStateException("invalid runtime intrinsic entry near line: " + rawLine); + } + final var identity = owner + "." + name + "@" + version; + final var previous = mapped.putIfAbsent(identity, id); + if (previous != null && previous != id) { + throw new IllegalStateException( + "runtime intrinsic identity collision for " + identity + ": " + previous + " vs " + id); + } + insideEntry = false; + } + } + return Map.copyOf(mapped); + } + + private int parseRuntimeId(final String rawId) { + if (rawId.startsWith("0x") || rawId.startsWith("0X")) { + return Integer.parseUnsignedInt(rawId.substring(2), 16); + } + return Integer.parseUnsignedInt(rawId, 10); + } + + private boolean strictParityEnabled() { + final var strict = System.getenv(STRICT_PARITY_ENV); + if (isTruthy(strict)) { + return true; + } + return isTruthy(System.getenv("CI")); + } + + private boolean isTruthy(final String value) { + if (value == null) { + return false; + } + return switch (value.trim().toLowerCase()) { + case "1", "true", "yes", "on" -> true; + default -> false; + }; + } + + private String parityDiff( + final Map runtimeSnapshot, + final Map compilerSnapshot) { + final var missingInCompiler = new ArrayList(); + final var missingInRuntime = new ArrayList(); + final var mismatchedIds = new ArrayList(); + for (final var runtimeEntry : runtimeSnapshot.entrySet()) { + final var compilerId = compilerSnapshot.get(runtimeEntry.getKey()); + if (compilerId == null) { + missingInCompiler.add(runtimeEntry.getKey()); + continue; + } + if (!runtimeEntry.getValue().equals(compilerId)) { + mismatchedIds.add(runtimeEntry.getKey() + " runtime=" + runtimeEntry.getValue() + " compiler=" + compilerId); + } + } + for (final var compilerEntry : compilerSnapshot.entrySet()) { + if (!runtimeSnapshot.containsKey(compilerEntry.getKey())) { + missingInRuntime.add(compilerEntry.getKey()); + } + } + return "intrinsic registry parity mismatch" + + "\nmissingInCompiler=" + List.copyOf(missingInCompiler) + + "\nmissingInRuntime=" + List.copyOf(missingInRuntime) + + "\nidMismatches=" + List.copyOf(mismatchedIds); + } +}