implements PR-08.5
This commit is contained in:
parent
505a4f72ce
commit
12a6602b0a
@ -1,11 +1,16 @@
|
|||||||
package p.studio.compiler.backend.irvm;
|
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.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
final class IRVMIntrinsicRegistry {
|
final class IRVMIntrinsicRegistry {
|
||||||
private static final Map<IntrinsicKey, Integer> FINAL_ID_BY_INTRINSIC = createRegistry();
|
private static final String REGISTRY_RESOURCE = "/intrinsics/registry-v1.csv";
|
||||||
|
private static final Map<IntrinsicKey, Integer> FINAL_ID_BY_INTRINSIC = loadRegistry();
|
||||||
|
|
||||||
private IRVMIntrinsicRegistry() {
|
private IRVMIntrinsicRegistry() {
|
||||||
}
|
}
|
||||||
@ -23,38 +28,79 @@ final class IRVMIntrinsicRegistry {
|
|||||||
return OptionalInt.of(resolved);
|
return OptionalInt.of(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<IntrinsicKey, Integer> createRegistry() {
|
static Map<String, Integer> snapshotByCanonicalIdentity() {
|
||||||
|
final var snapshot = new HashMap<String, Integer>(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<IntrinsicKey, Integer> 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<IntrinsicKey, Integer>();
|
final var registry = new HashMap<IntrinsicKey, Integer>();
|
||||||
register(registry, "vec2.dot", 1, 0x1000);
|
final var identityByFinalId = new HashMap<Integer, String>();
|
||||||
register(registry, "vec2.length", 1, 0x1001);
|
try (final var reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
|
||||||
|
String rawLine;
|
||||||
register(registry, "input.pad", 1, 0x2000);
|
int lineNumber = 0;
|
||||||
register(registry, "input.touch", 1, 0x2001);
|
while ((rawLine = reader.readLine()) != null) {
|
||||||
|
lineNumber++;
|
||||||
register(registry, "input.pad.up", 1, 0x2010);
|
final var line = rawLine.trim();
|
||||||
register(registry, "input.pad.down", 1, 0x2011);
|
if (line.isBlank() || line.startsWith("#")) {
|
||||||
register(registry, "input.pad.left", 1, 0x2012);
|
continue;
|
||||||
register(registry, "input.pad.right", 1, 0x2013);
|
}
|
||||||
register(registry, "input.pad.a", 1, 0x2014);
|
final var columns = line.split(",", -1);
|
||||||
register(registry, "input.pad.b", 1, 0x2015);
|
if (columns.length != 3) {
|
||||||
register(registry, "input.pad.x", 1, 0x2016);
|
throw new IllegalStateException("invalid intrinsic registry row at line " + lineNumber + ": " + line);
|
||||||
register(registry, "input.pad.y", 1, 0x2017);
|
}
|
||||||
register(registry, "input.pad.l", 1, 0x2018);
|
final var canonicalName = columns[0].trim();
|
||||||
register(registry, "input.pad.r", 1, 0x2019);
|
final long canonicalVersion;
|
||||||
register(registry, "input.pad.start", 1, 0x201A);
|
try {
|
||||||
register(registry, "input.pad.select", 1, 0x201B);
|
canonicalVersion = Long.parseLong(columns[1].trim());
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
register(registry, "input.touch.button", 1, 0x2020);
|
throw new IllegalStateException("invalid intrinsic version at line " + lineNumber + ": " + line, ex);
|
||||||
register(registry, "input.touch.x", 1, 0x2021);
|
}
|
||||||
register(registry, "input.touch.y", 1, 0x2022);
|
final int finalId = parseFinalId(columns[2].trim(), lineNumber, line);
|
||||||
|
register(registry, canonicalName, canonicalVersion, finalId);
|
||||||
register(registry, "input.button.pressed", 1, 0x2030);
|
final var identity = canonicalIdentity(canonicalName, canonicalVersion);
|
||||||
register(registry, "input.button.released", 1, 0x2031);
|
final var previous = identityByFinalId.putIfAbsent(finalId, identity);
|
||||||
register(registry, "input.button.down", 1, 0x2032);
|
if (previous != null && !previous.equals(identity)) {
|
||||||
register(registry, "input.button.hold", 1, 0x2033);
|
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);
|
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(
|
private static void register(
|
||||||
final Map<IntrinsicKey, Integer> registry,
|
final Map<IntrinsicKey, Integer> registry,
|
||||||
final String canonicalName,
|
final String canonicalName,
|
||||||
|
|||||||
@ -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
|
||||||
|
@ -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<String, Integer> parseRuntimeIntrinsicSnapshot(final Path runtimeFile) throws IOException {
|
||||||
|
final var runtimeSource = Files.readAllLines(runtimeFile, StandardCharsets.UTF_8);
|
||||||
|
final var mapped = new LinkedHashMap<String, Integer>();
|
||||||
|
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<String, Integer> runtimeSnapshot,
|
||||||
|
final Map<String, Integer> compilerSnapshot) {
|
||||||
|
final var missingInCompiler = new ArrayList<String>();
|
||||||
|
final var missingInRuntime = new ArrayList<String>();
|
||||||
|
final var mismatchedIds = new ArrayList<String>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user