From b5dcb1e8590a010a1b75da334f2bf234a9104fc0 Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 14:49:00 +0200 Subject: [PATCH 1/2] Support FastKeyedMap string-key intrinsics (PoC) --- .../interpreter/ILInterpreter.java | 14 ++- .../imtranslation/JassKeyedMapLowering.java | 90 +++++++++++++++---- .../imtranslation/LuaKeyedMap.java | 12 +-- .../resources/agent-docs/WURST_LANGUAGE.md | 4 +- .../wurstscript/tests/LuaKeyedMapTests.java | 89 +++++++++++++++++- .../java/tests/wurstscript/tests/StdLib.java | 2 +- 6 files changed, 181 insertions(+), 30 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java index 3c4fdc7f2..71c71d121 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java @@ -233,7 +233,9 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab // through the int fallback, with the value in its integer representation. if (JassKeyedMapLowering.isUnloweredPutNative(f)) { // Looked up first: it checks the intrinsic's shape before any argument is read. - ImFunction put = JassKeyedMapLowering.fallbackOf(globalState.getProg(), f); + ImFunction put = JassKeyedMapLowering.fallbackOf(globalState.getProg(), f, + f.getParameters().size() > 1 + ? keyedMapCallType(globalState, f, caller, f.getParameters().get(1).getType()) : JassIm.ImVoid()); JassKeyedMapLowering.checkSpecialization(f, keyedMapCallType(globalState, f, caller, f.getParameters().get(1).getType()), keyedMapCallType(globalState, f, caller, f.getParameters().get(2).getType())); @@ -241,13 +243,21 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab return new LocalState(); } if (JassKeyedMapLowering.isUnloweredGetNative(f)) { - ImFunction get = JassKeyedMapLowering.fallbackOf(globalState.getProg(), f); + ImFunction get = JassKeyedMapLowering.fallbackOf(globalState.getProg(), f, + f.getParameters().size() > 1 + ? keyedMapCallType(globalState, f, caller, f.getParameters().get(1).getType()) : JassIm.ImVoid()); ImType valueType = keyedMapCallType(globalState, f, caller, f.getReturnType()); JassKeyedMapLowering.checkSpecialization(f, keyedMapCallType(globalState, f, caller, f.getParameters().get(1).getType()), valueType); ILconst stored = runFunc(globalState, get, caller, args[0], args[1]).getReturnVal(); return new LocalState(keyedMapValueOfInt(globalState, stored, valueType)); } + if (JassKeyedMapLowering.isUnloweredMembership(f)) { + ImFunction fallback = JassKeyedMapLowering.membershipFallback(globalState.getProg(), f, + f.getParameters().size() > 1 + ? keyedMapCallType(globalState, f, caller, f.getParameters().get(1).getType()) : JassIm.ImVoid()); + return runFunc(globalState, fallback, caller, args); + } // --- local state & bind parameters --- LocalState localState = new LocalState(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/JassKeyedMapLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/JassKeyedMapLowering.java index d652ded54..226f67818 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/JassKeyedMapLowering.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/JassKeyedMapLowering.java @@ -32,6 +32,8 @@ public static void transform(ImProg prog) { lowerPut(prog, f); } else if (GET_NATIVE.equals(name)) { lowerGet(prog, f); + } else if ("keyedMapHasNative".equals(name) || "keyedMapRemoveNative".equals(name)) { + lowerMembership(prog, f); } } } @@ -50,48 +52,93 @@ public static boolean isUnloweredGetNative(ImFunction f) { return !f.getTypeVariables().isEmpty() && GET_NATIVE.equals(intrinsicName(f)); } + public static boolean isUnloweredMembership(ImFunction f) { + return !f.getTypeVariables().isEmpty() + && ("keyedMapHasNative".equals(intrinsicName(f)) || "keyedMapRemoveNative".equals(intrinsicName(f))); + } + + public static ImFunction membershipFallback(ImProg prog, ImFunction f, ImType keyType) { + boolean has = "keyedMapHasNative".equals(intrinsicName(f)); + if (f.getParameters().size() != 2 || !TypesHelper.isIntType(f.getParameters().get(0).getType()) + || !(has ? TypesHelper.isBoolType(f.getReturnType()) : f.getReturnType() instanceof ImVoid)) { + throw invalidSpecialization(f, "KeyedMap membership requires an int map and a handle or string key"); + } + if (!supportedKey(keyType)) { + throw invalidSpecialization(f, "KeyedMap supports only handle and string keys"); + } + String name = has ? "keyedMapHas" : "keyedMapRemove"; + if (TypesHelper.isStringType(keyType)) { + name += "String"; + } + for (ImFunction candidate : prog.getFunctions()) { + if (name.equals(intrinsicName(candidate)) && packageOf(candidate) == packageOf(f) + && candidate.getParameters().size() == 2 + && TypesHelper.isIntType(candidate.getParameters().get(0).getType()) + && sameKeyRepresentation(candidate.getParameters().get(1).getType(), keyType) + && (has ? TypesHelper.isBoolType(candidate.getReturnType()) : candidate.getReturnType() instanceof ImVoid)) { + return candidate; + } + } + throw invalidSpecialization(f, "The package must declare the " + name + " Jass fallback"); + } + + private static void lowerMembership(ImProg prog, ImFunction f) { + ImType keyType = f.getParameters().size() > 1 ? f.getParameters().get(1).getType() : JassIm.ImVoid(); + ImFunction fallback = membershipFallback(prog, f, keyType); + ImExpr call = JassIm.ImFunctionCall(f.attrTrace(), fallback, JassIm.ImTypeArguments(), + JassIm.ImExprs(JassIm.ImVarAccess(f.getParameters().get(0)), JassIm.ImVarAccess(f.getParameters().get(1))), + false, CallType.NORMAL); + f.getBody().clear(); + f.getLocals().clear(); + f.getBody().add(f.getReturnType() instanceof ImVoid ? call : JassIm.ImReturn(f.attrTrace(), call)); + } + /** * The int fallback that the lowered put or get calls, found the way this pass finds it. Checks the * (map, key[, value]) shape first and raises this pass's diagnostic when it is wrong, so running the * program before the lowering cannot bypass it. */ public static ImFunction fallbackOf(ImProg prog, ImFunction f) { + return fallbackOf(prog, f, f.getParameters().size() > 1 ? f.getParameters().get(1).getType() : JassIm.ImVoid()); + } + + public static ImFunction fallbackOf(ImProg prog, ImFunction f, ImType keyType) { boolean put = PUT_NATIVE.equals(intrinsicName(f)); boolean shaped = f.getParameters().size() == (put ? 3 : 2) && TypesHelper.isIntType(f.getParameters().get(0).getType()) && (!put || f.getReturnType() instanceof ImVoid); if (!shaped) { throw invalidSpecialization(f, put - ? "keyedMapPutNative requires an int map, a handle key, and an int-represented value" - : "keyedMapGetNative requires an int map, a handle key, and an int-represented result"); + ? "keyedMapPutNative requires an int map, a handle or string key, and an int-represented value" + : "keyedMapGetNative requires an int map, a handle or string key, and an int-represented result"); } - return findFallback(prog, packageOf(f), f, put ? PUT : GET_INT, put); + return findFallback(prog, packageOf(f), f, put ? PUT : GET_INT, put, keyType); } /** - * Raises this pass's diagnostic unless a call's resolved types are the ones it lowers: a handle + * Raises this pass's diagnostic unless a call's resolved types are the ones it lowers: a handle or string * key, and a value with Jass's integer representation, an int or a class reference. The * interpreter checks each call with it, so a specialization the Jass build rejects cannot pass * there. */ public static void checkSpecialization(ImFunction f, ImType keyType, ImType valueType) { - if (LuaNativeLowering.isHandleType(keyType) + if (supportedKey(keyType) && (TypesHelper.isIntType(valueType) || valueType instanceof ImClassType)) { return; } throw invalidSpecialization(f, PUT_NATIVE.equals(intrinsicName(f)) - ? "keyedMapPutNative requires an int map, a handle key, and an int-represented value" - : "keyedMapGetNative requires an int map, a handle key, and an int-represented result"); + ? "keyedMapPutNative requires an int map, a handle or string key, and an int-represented value" + : "keyedMapGetNative requires an int map, a handle or string key, and an int-represented result"); } private static void lowerPut(ImProg prog, ImFunction f) { if (f.getParameters().size() != 3 || !(f.getReturnType() instanceof ImVoid) || !TypesHelper.isIntType(f.getParameters().get(0).getType()) - || !LuaNativeLowering.isHandleType(f.getParameters().get(1).getType()) + || !supportedKey(f.getParameters().get(1).getType()) || !TypesHelper.isIntType(f.getParameters().get(2).getType())) { - throw invalidSpecialization(f, "keyedMapPutNative requires an int map, a handle key, and an int-represented value"); + throw invalidSpecialization(f, "keyedMapPutNative requires an int map, a handle or string key, and an int-represented value"); } - ImFunction fallback = findFallback(prog, packageOf(f), f, PUT, true); + ImFunction fallback = findFallback(prog, packageOf(f), f, PUT, true, f.getParameters().get(1).getType()); ImVar map = f.getParameters().get(0); ImVar key = f.getParameters().get(1); ImVar value = f.getParameters().get(2); @@ -105,10 +152,10 @@ private static void lowerPut(ImProg prog, ImFunction f) { private static void lowerGet(ImProg prog, ImFunction f) { if (f.getParameters().size() != 2 || !TypesHelper.isIntType(f.getReturnType()) || !TypesHelper.isIntType(f.getParameters().get(0).getType()) - || !LuaNativeLowering.isHandleType(f.getParameters().get(1).getType())) { - throw invalidSpecialization(f, "keyedMapGetNative requires an int map, a handle key, and an int-represented result"); + || !supportedKey(f.getParameters().get(1).getType())) { + throw invalidSpecialization(f, "keyedMapGetNative requires an int map, a handle or string key, and an int-represented result"); } - ImFunction fallback = findFallback(prog, packageOf(f), f, GET_INT, false); + ImFunction fallback = findFallback(prog, packageOf(f), f, GET_INT, false, f.getParameters().get(1).getType()); ImVar map = f.getParameters().get(0); ImVar key = f.getParameters().get(1); ImExpr call = JassIm.ImFunctionCall(f.attrTrace(), fallback, JassIm.ImTypeArguments(), @@ -118,7 +165,10 @@ private static void lowerGet(ImProg prog, ImFunction f) { f.getBody().add(JassIm.ImReturn(f.attrTrace(), call)); } - private static ImFunction findFallback(ImProg prog, WPackage owner, ImFunction source, String name, boolean put) { + private static ImFunction findFallback(ImProg prog, WPackage owner, ImFunction source, String name, boolean put, ImType keyType) { + if (TypesHelper.isStringType(keyType)) { + name = put ? "keyedMapPutString" : "keyedMapGetStringInt"; + } if (owner != null) { for (ImFunction candidate : prog.getFunctions()) { if (!name.equals(intrinsicName(candidate)) || packageOf(candidate) != owner) { @@ -127,12 +177,12 @@ private static ImFunction findFallback(ImProg prog, WPackage owner, ImFunction s boolean valid = put ? candidate.getParameters().size() == 3 && TypesHelper.isIntType(candidate.getParameters().get(0).getType()) - && isHandle(candidate.getParameters().get(1).getType()) + && sameKeyRepresentation(candidate.getParameters().get(1).getType(), keyType) && TypesHelper.isIntType(candidate.getParameters().get(2).getType()) && candidate.getReturnType() instanceof ImVoid : candidate.getParameters().size() == 2 && TypesHelper.isIntType(candidate.getParameters().get(0).getType()) - && isHandle(candidate.getParameters().get(1).getType()) + && sameKeyRepresentation(candidate.getParameters().get(1).getType(), keyType) && TypesHelper.isIntType(candidate.getReturnType()); if (!valid) { throw new CompileError(candidate.attrTrace().attrErrorPos(), @@ -150,6 +200,14 @@ private static boolean isHandle(ImType type) { return type instanceof ImSimpleType simple && "handle".equals(simple.getTypename()); } + private static boolean supportedKey(ImType type) { + return LuaNativeLowering.isHandleType(type) || TypesHelper.isStringType(type); + } + + private static boolean sameKeyRepresentation(ImType fallback, ImType keyType) { + return TypesHelper.isStringType(keyType) ? TypesHelper.isStringType(fallback) : isHandle(fallback); + } + private static String intrinsicName(ImFunction f) { return f.attrTrace() instanceof FuncDef fd && fd.attrHasAnnotation(CompilerIntrinsics.ANNOTATION) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedMap.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedMap.java index 353185a1a..c044de110 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedMap.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/LuaKeyedMap.java @@ -92,17 +92,17 @@ public static String nativeStubFor(ImFunction f) { return switch (fd.getName()) { case CREATE -> f.getParameters().isEmpty() && TypesHelper.isIntType(result) ? NATIVE_CREATE : null; - case PUT, PUT_NATIVE -> isKeyedPair(f, 3) && isValueType(f.getParameters().get(2).getType()) + case PUT, PUT_NATIVE, "keyedMapPutString" -> isKeyedPair(f, 3) && isValueType(f.getParameters().get(2).getType()) && result instanceof ImVoid ? NATIVE_PUT : null; case GET -> isKeyedPair(f, 2) && isNilDefaultedType(result) ? NATIVE_GET : null; case GET_NATIVE -> isKeyedPair(f, 2) ? nativeGetStub(result) : null; - case GET_INT -> isKeyedPair(f, 2) && TypesHelper.isIntType(result) ? NATIVE_GET_INT : null; + case GET_INT, "keyedMapGetStringInt" -> isKeyedPair(f, 2) && TypesHelper.isIntType(result) ? NATIVE_GET_INT : null; case GET_REAL -> isKeyedPair(f, 2) && TypesHelper.isRealType(result) ? NATIVE_GET_REAL : null; case GET_BOOL -> isKeyedPair(f, 2) && TypesHelper.isBoolType(result) ? NATIVE_GET_BOOL : null; case GET_STR -> isKeyedPair(f, 2) && TypesHelper.isStringType(result) ? NATIVE_GET_STR : null; - case HAS -> isKeyedPair(f, 2) && TypesHelper.isBoolType(result) ? NATIVE_HAS : null; - case REMOVE -> isKeyedPair(f, 2) && result instanceof ImVoid ? NATIVE_REMOVE : null; + case HAS, "keyedMapHasNative", "keyedMapHasString" -> isKeyedPair(f, 2) && TypesHelper.isBoolType(result) ? NATIVE_HAS : null; + case REMOVE, "keyedMapRemoveNative", "keyedMapRemoveString" -> isKeyedPair(f, 2) && result instanceof ImVoid ? NATIVE_REMOVE : null; case DESTROY -> isDestroy(f) ? NATIVE_DESTROY : null; default -> null; }; @@ -126,14 +126,14 @@ private static boolean isKeyedPair(ImFunction f, int count) { } /** - * Key types: an int, an erased type parameter which is the element itself, or a concrete + * Key types: an int, a string, an erased type parameter which is the element itself, or a concrete * handle type. The last lets a library declare the operations with a {@code unit} key and a * Jass body of {@code Table} plus {@code GetHandleId}, which is correct in source on every * compiler, so a build which does not lower the operations degrades to the hashtable path * instead of keying every element the same. */ private static boolean isKeyType(ImType t) { - return TypesHelper.isIntType(t) || t instanceof ImTypeVarRef || t instanceof ImAnyType + return TypesHelper.isIntType(t) || TypesHelper.isStringType(t) || t instanceof ImTypeVarRef || t instanceof ImAnyType || LuaNativeLowering.isHandleType(t); } diff --git a/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md b/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md index 546e38c5c..e9a6f0ae5 100644 --- a/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md +++ b/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md @@ -194,7 +194,9 @@ For the same reason, an old-style generic (`Box`, `toInt`) cannot be insta ### KeyedMap intrinsic representation -The compiler-owned `keyedMapPut`/`keyedMapGet` intrinsics have a map handle stored as `int`, a key represented by the native handle (or supported primitive key), and a value represented by its specialized native type. Keep their intrinsic declarations and signatures aligned with the shapes recognized by `LuaKeyedMap`; changing the declaration shape can prevent intrinsic recognition. A wrapper such as `KeyedMap` should forward `K` and `V` directly, without `GetHandleId`, integer casts, or class-to-index boxing on the Lua path. +The compiler-owned `keyedMapPut`/`keyedMapGet` intrinsics have a map handle stored as `int`, a key represented by the native handle (or supported primitive key), and a value represented by its specialized native type. Keep their intrinsic declarations and signatures aligned with the shapes recognized by `LuaKeyedMap`; changing the declaration shape can prevent intrinsic recognition. `FastKeyedMap` forwards `K` and `V` directly, without `GetHandleId`, integer casts, or class-to-index boxing on the Lua path. Its supported key types are handles and strings. The generic `keyedMapHasNative` and `keyedMapRemoveNative` accompany generic put/get; the fixed handle operations remain available to existing callers. + +On Jass, string-key specializations select the same-package annotated fallbacks `keyedMapPutString`, `keyedMapGetStringInt`, `keyedMapHasString`, and `keyedMapRemoveString`. These take an int map and string key, with int values/results for put/get. The library uses `StringHash` only to choose a bucket and checks full string equality inside that bucket; a hash is never treated as a unique key. Removal frees its collision entry; destruction frees every remaining entry through an ownership list before releasing the backing Table. The interpreter selects the same fallbacks using resolved generic arguments. On Lua these operations use the string itself as a native table key, with no hashing or collision entries emitted. A low-level map must consistently use one key representation; mixing legacy handle operations with string operations on the same map is unsupported. On Lua, the recognized operations lower to a native table keyed by the handle object itself: put is one `t[k] = v` store and get is one `t[k]` read (with typed defaults for primitive results). Do not introduce handle-id conversion or an index registry on this path. The source/Jass fallback still uses the hashtable-backed `Table` API and `GetHandleId`, because Jass has no Lua table identity keys. Jass supports only `int` and class-reference values through the existing int representation; other specializations are rejected rather than boxed. Generic reference values must be non-null; use `remove` for absence. Destroy releases the Jass `Table` and clears the Lua table in place, since aliases to a destroyed Lua object may still retain its store. `LuaKeyedMapTests` covers intrinsic recognition, direct Lua table access, native handle identity, generic integer/class values, Jass fallback, and retained-alias destruction; extend it when changing this lowering contract. diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java index a8ba9b1a0..93b52df31 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java @@ -25,6 +25,87 @@ */ public class LuaKeyedMapTests extends WurstScriptTest { + @Test + public void stringKeysInFastKeyedMap() throws IOException { + test().withStdLib().testLua(true).luaOnly(false).inline().executeProg().lines( + "package Test", + "import KeyedMap", + "init", + " let m = new FastKeyedMap()", + " let other = new FastKeyedMap()", + " m.put(\"alpha\", 42)", + " m.put(\"ALPHA\", 7)", + " m.put(\"\", 0)", + " m.put(\"alpha\", 43)", + " if m.get(\"alpha\") != 43 or m.get(\"ALPHA\") != 7 or not m.has(\"\")", + " testFail(\"string keys or stored zero\")", + " if other.has(\"alpha\") or m.has(\"missing\") or m.get(\"missing\") != 0", + " testFail(\"absence or isolation\")", + " m.remove(\"alpha\")", + " m.remove(\"missing\")", + " if m.has(\"alpha\") or m.get(\"alpha\") != 0 or m.get(\"ALPHA\") != 7", + " testFail(\"remove\")", + " destroy m", + " let reused = new FastKeyedMap()", + " if reused.has(\"ALPHA\") or reused.has(\"\")", + " testFail(\"destroy and reuse\")", + " destroy reused", + " destroy other", + " testSuccess()", + "endpackage"); + String lua = getFunctionBody(compiled("stringKeysInFastKeyedMap"), "init_Test"); + assertFalse(lua.contains("StringHash(")); + assertFalse(lua.contains("GetHandleId(")); + assertFalse(lua.contains("keyedMapPutString(")); + assertTrue(lua, lua.contains("[\"alpha\"]")); + } + + @Test + public void stringKeysWithClassValues() { + test().withStdLib().testLua(true).luaOnly(false).executeProg().lines( + "package Test", + "import KeyedMap", + "class Data", + " int value = 17", + "init", + " let m = new FastKeyedMap()", + " let data = new Data()", + " m.put(\"data\", data)", + " if m.get(\"data\") != data or m.get(\"data\").value != 17 or m.get(\"absent\") != null", + " testFail(\"class values\")", + " m.remove(\"data\")", + " if m.has(\"data\") or m.get(\"data\") != null", + " testFail(\"class remove\")", + " destroy data", + " destroy m", + " testSuccess()", + "endpackage"); + } + + @Test + public void stdlibKeyedMapTests() { + test().withStdLib().executeTests().lines( + "package Test", + "import KeyedMapTests", + "endpackage"); + } + + @Test + public void stringMapEmissionIsDeterministic() throws IOException { + String[] source = { + "package Test", "import KeyedMap", "init", + " let m = new FastKeyedMap()", + " m.put(\"alpha\", 0)", " m.remove(\"alpha\")", " destroy m", "endpackage" + }; + File jassFile = new File("test-output/LuaKeyedMapTests_stringMapEmissionIsDeterministic_no_opts.j"); + test().withStdLib().testLua(true).luaOnly(false).lines(source); + String firstLua = compiled("stringMapEmissionIsDeterministic"); + String firstJass = Files.toString(jassFile, Charsets.UTF_8); + test().withStdLib().testLua(true).luaOnly(false).lines(source); + org.testng.Assert.assertEquals(compiled("stringMapEmissionIsDeterministic"), firstLua); + org.testng.Assert.assertEquals(Files.toString(jassFile, Charsets.UTF_8), firstJass); + } + private String getFunctionBody(String output, String functionName) { // Up to the closing 'end' at column 0; nested blocks are indented. Pattern pattern = Pattern.compile("function\\s*" + functionName + "\\s*\\([^\\n]*\\n(.*?)\\nend", Pattern.DOTALL); @@ -537,7 +618,7 @@ public void malformedGenericPutIsReportedWhenInterpreted() { " keyedMapPutNative((new Table()) castTo int, CreateUnit(Player(0), 'hfoo', 0., 0., 0.))", "endpackage")); assertTrue(failure.getMessage(), failure.getMessage() - .contains("keyedMapPutNative requires an int map, a handle key, and an int-represented value")); + .contains("keyedMapPutNative requires an int map, a handle or string key, and an int-represented value")); assertFalse(failure.getMessage(), failure.getMessage().contains("ArrayIndexOutOfBounds")); } @@ -555,7 +636,7 @@ public void handleValuedGenericPutIsReportedWhenInterpreted() { " keyedMapPutNative((new Table()) castTo int, CreateUnit(Player(0), 'hfoo', 0., 0., 0.), null)", "endpackage")); assertTrue(failure.getMessage(), failure.getMessage() - .contains("keyedMapPutNative requires an int map, a handle key, and an int-represented value")); + .contains("keyedMapPutNative requires an int map, a handle or string key, and an int-represented value")); } /** An unbounded key type called with an int key is rejected before the fallback's handle parameter. */ @@ -572,7 +653,7 @@ public void intKeyedGenericGetIsReportedWhenInterpreted() { " let value = keyedMapGetNative((new Table()) castTo int, 7)", "endpackage")); assertTrue(failure.getMessage(), failure.getMessage() - .contains("keyedMapGetNative requires an int map, a handle key, and an int-represented result")); + .contains("keyedMapGetNative requires an int map, a handle or string key, and an int-represented result")); } /** The generic value intrinsics and a typed wrapper over them, as the standard library declares them. */ @@ -841,7 +922,7 @@ public void handleBoundGenericValuesKeepJassHashtableFallback() throws IOExcepti @Test public void jassRejectsValuesWithoutIntegerRepresentation() { - test().expectError("keyedMapPutNative requires an int map, a handle key, and an int-represented value") + test().expectError("keyedMapPutNative requires an int map, a handle or string key, and an int-represented value") .withStdLib().lines( "package KeyedMap", "import Table", diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java index 94d89176a..28c910428 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java @@ -24,7 +24,7 @@ public class StdLib { /** * version to use for the tests */ - private final static String version = "18c454c38e5f32a6d6e51acbd63cc53c9e915402"; + private final static String version = "4523969eb42c46e0d49cc7f6b3c363a9332fad02"; /** * flag so that initialization in only done once From a691fc3b8195dfde9fcaa954a2c922881b6e60cc Mon Sep 17 00:00:00 2001 From: Frotty Date: Fri, 9 Oct 2026 15:33:53 +0200 Subject: [PATCH 2/2] Report string-key signatures for malformed map fallbacks --- .../imtranslation/JassKeyedMapLowering.java | 3 ++- .../wurstscript/tests/LuaKeyedMapTests.java | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/JassKeyedMapLowering.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/JassKeyedMapLowering.java index 226f67818..e2b085bc9 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/JassKeyedMapLowering.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/JassKeyedMapLowering.java @@ -186,7 +186,8 @@ && sameKeyRepresentation(candidate.getParameters().get(1).getType(), keyType) && TypesHelper.isIntType(candidate.getReturnType()); if (!valid) { throw new CompileError(candidate.attrTrace().attrErrorPos(), - name + " must keep the existing int-map/handle-key signature."); + name + " must keep the existing int-map/" + + (TypesHelper.isStringType(keyType) ? "string" : "handle") + "-key signature."); } return candidate; } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java index 93b52df31..418806c15 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaKeyedMapTests.java @@ -25,6 +25,32 @@ */ public class LuaKeyedMapTests extends WurstScriptTest { + @Test + public void malformedStringPutFallbackReportsStringSignature() { + test().expectError("keyedMapPutString must keep the existing int-map/string-key signature.").lines( + "package Test", + "@compilerintrinsic function keyedMapPutString(int map, handle key, int value)", + " skip", + "@compilerintrinsic function keyedMapPutNative(int map, K key, V value)", + " skip", + "init", + " keyedMapPutNative(0, \"key\", 1)", + "endpackage"); + } + + @Test + public void malformedStringGetFallbackReportsStringSignature() { + test().expectError("keyedMapGetStringInt must keep the existing int-map/string-key signature.").lines( + "package Test", + "@compilerintrinsic function keyedMapGetStringInt(int map, handle key) returns int", + " return 0", + "@compilerintrinsic function keyedMapGetNative(int map, K key) returns V", + " return null", + "init", + " let value = keyedMapGetNative(0, \"key\")", + "endpackage"); + } + @Test public void stringKeysInFastKeyedMap() throws IOException { test().withStdLib().testLua(true).luaOnly(false).inline().executeProg().lines(