Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 49 additions & 18 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,35 @@ Notes rather than leaving it in a commit message.
Numbering is stable: finished items leave a gap rather than shifting the ones below,
because `LOOP.md` refers to items by number.

3. **`slotFor` is bound to `get`'s implementation** in the emitted Lua — confirmed real, and
not an artefact of item 1: it survives sanitisation unchanged. Diagnosed, not yet fixed.
The alias sets in `LuaDispatchPreparation` decide which slots a method claims, and
`sharesSemanticName` accepts a match on *either* of two names: the source name (`get`,
`slotFor`) or `semanticNameFromMethodName`, which is the substring after the last
underscore. For a specialised method that substring is a type-argument fragment —
`FastHashMap_get_specialized__integer__integer___integer` and the `slotFor` one both yield
`integer` — so two unrelated methods count as sharing a name. They also share a dispatch
signature here (`(pos) returns int` both), which is the other half of the guard, so `get`
claims `slotFor`'s slot. Fix: when both methods have a real source name, that should decide;
the substring heuristic is a fallback for when there is no trace to ask, not an alternative.
Watch the closure and bridge cases in `TypeClassTests`/`LuaBackendAuditTests` — they are what
the loose match was presumably widened for.

4. **Finish the FastHashMap proof.** `FastHashMapTests` is the first real use of bounds.
Add `remove` with tombstones, and an assertion that the emitted code stays cheap: no
dispatch node, no instance dictionary, and no WC3 hashtable natives — array access only,
which is the whole point versus `HashMap extends Table`.
16. **A never-written array of a type parameter reads as nothing, silently.** In the interpreter
only — Jass and Lua both give the type argument's default. `DefaultValue.get(ImTypeVarRef)`
returns `ILconstUnsafeDefault`, whose `isEqualTo` matches only another `ILconstUnsafeDefault`,
so a comparison against the real default is quietly false rather than an error. Repro:

class Box<T:>
private static T array none
static function first() returns T
return none[0]
init
if Box<int>.first() == 0
testSuccess()

Passes on every Jass configuration and fails on the pre-transform interpreter run. The plain
`int array` version passes, so this is specific to the type parameter. The interpreter knows
the current type argument (`ProgramState.resolveType`), but `DefaultValue` is a static
attribute with no access to it, and the array's default supplier is bound when the array is
allocated rather than when it is read. Either resolve at read time where the state is in hand,
or make the placeholder throw when used — what it must not do is compare unequal in silence.
Found by `FastHashMapTests`: the tombstone fixture needs a "no value" for `V`.

15. **One junk dispatch slot per specialised class.** Left over from item 3, same heuristic in
the other place it is used. `addDirectAliases` composes `owner.getName() + "_" +
semanticNameFromMethodName(name)`, and for a specialised method that trailing segment is the
type argument, so every method of `FastHashMap<int, int>` claims the same
`FastHashMap_specialized_integer__integer_integer` slot and the alphabetically first wins.
Nothing calls it, so it is dead weight rather than a wrong result — but it is the same
mistake, and the alias it *should* produce is the class qualified with the declared name.
Fixing it changes emitted slot names, so it wants its own commit and its own suite run.

5. **Lua dispatch inside a closure.** Works on Jass since #1229. On Lua the specialised class
is built correctly but nothing calls it, because the closure is reached through its
Expand Down Expand Up @@ -124,6 +135,26 @@ because `LOOP.md` refers to items by number.

## Done

- 4. The FastHashMap proof is complete. `remove` leaves a tombstone, which `slotFor` passes over
when searching and reuses when putting; the probe is bounded by capacity rather than running
until it finds a gap, so a table full of tombstones cannot spin. `emittedCodeCostsNothingExtra`
asserts the cost claim on the *least* optimised configuration, because it has to hold by
construction rather than by inlining: storage is four plain Jass arrays, no WC3 hashtable native
is reached for, `slotFor` takes nothing beyond the receiver and the key — no instance is threaded
through at runtime — and both requirements are direct calls, not `ExecuteFunc`, not a dispatch
wrapper. In the optimised output `hash(key)` becomes `key` and `equals(a, key)` becomes `a != key`.
The `dispatch_` functions that remain are the nullpointer check every Wurst class method gets,
not type class dispatch; the test is careful to look at the real function under that wrapper.
- 3. `slotFor`'s slot no longer holds `get`'s implementation. The alias sets in
`LuaDispatchPreparation` decide which slots a method claims, and `sharesSemanticName` accepted
a match on either the declared name or `semanticNameFromMethodName` — the substring after the
last underscore, which for a specialised method is a fragment of the type argument. Both
`FastHashMap_get_specialized_integer__integer` and the `slotFor` one end in `integer`, and the
two share a dispatch signature, so `get` claimed `slotFor`'s slot. Declared names now settle it
whenever both methods have one; the substring is a fallback for closures and bridges, which
have no declaration to ask. `FastHashMapTests.fastHashMapRuntimeLua` asserts every slot named
after a method binds that method's implementation — it fails on the old code with exactly the
binding above.
- 14. The Lua execution harness no longer hangs. It read the spawned interpreter's stderr to EOF
before touching stdout, and never bounded the wait, so a program that filled the stdout pipe
deadlocked the whole suite with no output and no timeout — a stray JVM was still sitting on it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ private static void addHierarchyAliases(ImMethod method, Set<String> aliases, Ma
return;
}
String dispatchKey = dispatchSignatureKey(method);
collectHierarchyAliases(owner, dispatchKey, semanticNames, aliases, sortedMethodsByClass, new HashSet<>());
collectHierarchyAliases(owner, method, dispatchKey, semanticNames, aliases, sortedMethodsByClass, new HashSet<>());
}

private static void collectHierarchyAliases(ImClass c, String dispatchKey, Set<String> semanticNames, Set<String> aliases,
private static void collectHierarchyAliases(ImClass c, ImMethod method, String dispatchKey, Set<String> semanticNames, Set<String> aliases,
Map<ImClass, List<ImMethod>> sortedMethodsByClass, Set<ImClass> visited) {
if (c == null || !visited.add(c)) {
return;
Expand All @@ -188,7 +188,7 @@ private static void collectHierarchyAliases(ImClass c, String dispatchKey, Set<S
if (!dispatchKey.equals(dispatchSignatureKey(candidate))) {
continue;
}
if (!sharesSemanticName(candidate, semanticNames)) {
if (!sharesSemanticName(method, candidate, semanticNames)) {
continue;
}
String candidateName = candidate.getName();
Expand All @@ -198,7 +198,7 @@ private static void collectHierarchyAliases(ImClass c, String dispatchKey, Set<S
}
}
for (ImClassType sc : c.getSuperClasses()) {
collectHierarchyAliases(sc.getClassDef(), dispatchKey, semanticNames, aliases, sortedMethodsByClass, visited);
collectHierarchyAliases(sc.getClassDef(), method, dispatchKey, semanticNames, aliases, sortedMethodsByClass, visited);
}
}

Expand All @@ -221,7 +221,7 @@ private static void addClosureFamilyAliases(ImProg prog, ImMethod method, Set<St
if (!runtimeKey.equals(closureRuntimeDispatchKey(candidate))) {
continue;
}
if (!sharesSemanticName(candidate, semanticNames)) {
if (!sharesSemanticName(method, candidate, semanticNames)) {
continue;
}
String candidateName = candidate.getName();
Expand All @@ -247,6 +247,24 @@ private static Set<String> semanticNames(ImMethod method) {
return names;
}

/**
* Whether {@code candidate} is the same method as {@code method} under a different name, which
* is what makes it worth claiming its slot.
*
* <p>When both were declared in source, their declared names settle it. The name-derived
* fallback below reads the segment after the last underscore, which for a specialised method
* is a fragment of the type argument: two unrelated methods of one specialisation both end in
* {@code integer} and would otherwise be taken for one another.
*/
private static boolean sharesSemanticName(ImMethod method, ImMethod candidate, Set<String> semanticNames) {
String declared = declaredName(method);
String candidateDeclared = declaredName(candidate);
if (!declared.isEmpty() && !candidateDeclared.isEmpty()) {
return declared.equals(candidateDeclared);
}
return sharesSemanticName(candidate, semanticNames);
}

private static boolean sharesSemanticName(ImMethod method, Set<String> semanticNames) {
if (semanticNames.isEmpty()) {
return false;
Expand All @@ -255,6 +273,21 @@ private static boolean sharesSemanticName(ImMethod method, Set<String> semanticN
|| semanticNames.contains(sourceSemanticName(method));
}

/** The name the method was written with, or empty when there is no declaration to ask. */
private static String declaredName(ImMethod method) {
if (method == null) {
return "";
}
de.peeeq.wurstscript.ast.Element trace = method.attrTrace();
if (trace instanceof FuncDef funcDef) {
return funcDef.getName();
}
if (trace instanceof AstElementWithFuncName withFuncName) {
return withFuncName.getFuncNameId().getName();
}
return "";
}

private static List<ImMethod> sortedMethodsForClass(ImClass c, Map<ImClass, List<ImMethod>> sortedMethodsByClass) {
return sortedMethodsByClass.computeIfAbsent(c, key -> {
List<ImMethod> methods = new ArrayList<>(key.getMethods());
Expand Down
Loading
Loading