Skip to content

Commit d78442a

Browse files
committed
Merge branch 'master' into less-garbage
2 parents 949489b + 0e4e9f1 commit d78442a

11 files changed

Lines changed: 337 additions & 17 deletions

File tree

Wurstpack/wurstscript/grill.cmd

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
@echo off
2-
setlocal
2+
setlocal EnableExtensions DisableDelayedExpansion
33

4-
rem Save current code page
5-
for /f "tokens=2 delims=: " %%A in ('chcp') do set "_OLDCP=%%A"
4+
rem Save current code page (extract number after ':')
5+
for /f "tokens=2 delims=:" %%A in ('chcp') do for /f "tokens=1" %%B in ("%%A") do set "_OLDCP=%%B"
66

77
rem Switch to UTF-8 for this session
88
chcp 65001 >NUL
@@ -38,3 +38,4 @@ rem Restore previous code page if we captured it
3838
if defined _OLDCP chcp %_OLDCP% >NUL
3939

4040
endlocal
41+
exit /b %ERRORLEVEL%

Wurstpack/wurstscript/wurstscript.cmd

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
@echo off
2-
setlocal
2+
setlocal EnableExtensions DisableDelayedExpansion
33

4-
rem Save current code page
5-
for /f "tokens=2 delims=: " %%A in ('chcp') do set "_OLDCP=%%A"
4+
rem Save current code page (extract number after ':')
5+
for /f "tokens=2 delims=:" %%A in ('chcp') do for /f "tokens=1" %%B in ("%%A") do set "_OLDCP=%%B"
66

77
rem Switch to UTF-8
88
chcp 65001 >NUL
@@ -34,3 +34,4 @@ if not exist "%JAVA%" (
3434
:restore
3535
if defined _OLDCP chcp %_OLDCP% >NUL
3636
endlocal
37+
exit /b %ERRORLEVEL%

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,11 +405,13 @@ protected CompilationResult compileScript(ModelManager modelManager, WurstGui gu
405405
WurstProjectConfigData projectConfigData, File buildDir,
406406
boolean isProd) throws Exception {
407407

408-
// Ensure we're working with the cached map
409-
File cachedMap = ensureCachedMap(gui);
408+
if (!runArgs.isHotReload()) {
409+
// Ensure we're working with the cached map
410+
File cachedMap = ensureCachedMap(gui);
410411

411-
// Update testMap to point to cached map
412-
testMap = Optional.of(cachedMap);
412+
// Update testMap to point to cached map
413+
testMap = Optional.of(cachedMap);
414+
}
413415

414416
CompilationResult result;
415417

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateClasses.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,9 +628,25 @@ private void replaceMethodCall(ImMethodCall mc) {
628628
ImExprs arguments = JassIm.ImExprs(receiver);
629629
arguments.addAll(mc.getArguments().removeAll());
630630

631-
ImFunction dispatch = dispatchFuncs.get(mc.getMethod());
631+
ImMethod method = mc.getMethod();
632+
// Fast path: with unchecked dispatch, a monomorphic method call can be lowered
633+
// directly to its implementation function.
634+
if (!checkedDispatch
635+
&& !method.getIsAbstract()
636+
&& method.getSubMethods().isEmpty()) {
637+
mc.replaceBy(JassIm.ImFunctionCall(
638+
mc.getTrace(),
639+
method.getImplementation(),
640+
JassIm.ImTypeArguments(),
641+
arguments,
642+
false,
643+
CallType.NORMAL));
644+
return;
645+
}
646+
647+
ImFunction dispatch = dispatchFuncs.get(method);
632648
if (dispatch == null) {
633-
throw new CompileError(mc.attrTrace().attrSource(), "Could not find dispatch for " + mc.getMethod().getName());
649+
throw new CompileError(mc.attrTrace().attrSource(), "Could not find dispatch for " + method.getName());
634650
}
635651
mc.replaceBy(JassIm.ImFunctionCall(mc.getTrace(), dispatch, JassIm.ImTypeArguments(), arguments, false, CallType.NORMAL));
636652

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,8 @@ private boolean isGlobalInitStmt(ImSet s, ImVar v) {
955955
}
956956

957957
private void collectGenericUsages(Element element) {
958+
// Cache expensive recursive submethod checks within this traversal.
959+
Map<ImMethod, Boolean> hasGenericSubmethodCache = new IdentityHashMap<>();
958960
element.accept(new Element.DefaultVisitor() {
959961
@Override
960962
public void visit(ImFunctionCall f) {
@@ -967,7 +969,22 @@ public void visit(ImFunctionCall f) {
967969
@Override
968970
public void visit(ImMethodCall mc) {
969971
super.visit(mc);
970-
if (!mc.getTypeArguments().isEmpty()) {
972+
ImMethod method = mc.getMethod();
973+
boolean hasTypeArgs = !mc.getTypeArguments().isEmpty();
974+
boolean needsDispatchSpecialization = false;
975+
// If type args are present, specialization is unconditional, so avoid extra checks.
976+
if (!hasTypeArgs) {
977+
// Interface/base dispatch methods can be non-generic but still require specialization
978+
// when they dispatch to generic implementors.
979+
needsDispatchSpecialization = methodImplementationIsGeneric(method);
980+
if (!needsDispatchSpecialization) {
981+
needsDispatchSpecialization = hasGenericSubmethodCache.computeIfAbsent(
982+
method,
983+
EliminateGenerics.this::hasGenericSubmethodImplementation
984+
);
985+
}
986+
}
987+
if (hasTypeArgs || needsDispatchSpecialization) {
971988
dbg("COLLECT GenericMethodCall: method=" + mc.getMethod().getName() + " " + id(mc.getMethod())
972989
+ " impl=" + (mc.getMethod().getImplementation() == null ? "null" : (mc.getMethod().getImplementation().getName() + " " + id(mc.getMethod().getImplementation())))
973990
+ " owningClass=" + (mc.getMethod().attrClass() == null ? "null" : (mc.getMethod().attrClass().getName() + " " + id(mc.getMethod().attrClass())))
@@ -1109,6 +1126,30 @@ public void visit(ImTypeIdOfClass f) {
11091126
});
11101127
}
11111128

1129+
private boolean methodImplementationIsGeneric(ImMethod method) {
1130+
ImFunction implementation = method.getImplementation();
1131+
return implementation != null && !implementation.getTypeVariables().isEmpty();
1132+
}
1133+
1134+
private boolean hasGenericSubmethodImplementation(ImMethod method) {
1135+
return hasGenericSubmethodImplementation(method, Collections.newSetFromMap(new IdentityHashMap<>()));
1136+
}
1137+
1138+
private boolean hasGenericSubmethodImplementation(ImMethod method, Set<ImMethod> visited) {
1139+
if (!visited.add(method)) {
1140+
return false;
1141+
}
1142+
for (ImMethod subMethod : method.getSubMethods()) {
1143+
if (methodImplementationIsGeneric(subMethod)) {
1144+
return true;
1145+
}
1146+
if (hasGenericSubmethodImplementation(subMethod, visited)) {
1147+
return true;
1148+
}
1149+
}
1150+
return false;
1151+
}
1152+
11121153
static boolean isGenericType(ImType type) {
11131154
return type.match(new ImType.Matcher<Boolean>() {
11141155
@Override

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StackTraceInjector2.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -380,14 +380,15 @@ private void rewriteFuncRefs(final List<ImFuncRefOrCall> funcRefs, Set<ImFunctio
380380
ImExprs args = JassIm.ImExprs(str);
381381
ImStmts body = bridgeFunc.getBody();
382382
de.peeeq.wurstscript.ast.Element trace = frTrace;
383-
// reset stack and add information for callback:
384-
body.add(JassIm.ImSet(trace, JassIm.ImVarAccess(stackSize), JassIm.ImIntVal(0)));
385383

386384
ImFunctionCall call = JassIm.ImFunctionCall(frTrace, f, JassIm.ImTypeArguments(), args, true, CallType.NORMAL);
387385
if (bridgeFunc.getReturnType() instanceof ImVoid) {
388386
stmt = call;
389387
} else {
390-
stmt = JassIm.ImReturn(frTrace, call);
388+
ImVar bridgeReturn = JassIm.ImVar(trace, bridgeFunc.getReturnType().copy(), "bridge_return", false);
389+
bridgeFunc.getLocals().add(bridgeReturn);
390+
body.add(JassIm.ImSet(trace, JassIm.ImVarAccess(bridgeReturn), call));
391+
stmt = JassIm.ImReturn(frTrace, JassIm.ImVarAccess(bridgeReturn));
391392
}
392393
body.add(stmt);
393394

@@ -426,7 +427,7 @@ private void rewriteErrorStatements(final Multimap<ImFunction, ImGetStackTrace>
426427
ImVar traceLimit = JassIm.ImVar(trace, TypesHelper.imInt(), "stacktraceLimit", false);
427428
f.getLocals().add(traceLimit);
428429
ImStmts stmts = JassIm.ImStmts();
429-
stmts.add(JassIm.ImSet(trace, JassIm.ImVarAccess(traceStr), JassIm.ImStringVal("")));
430+
stmts.add(JassIm.ImSet(trace, JassIm.ImVarAccess(traceStr), JassIm.ImStringVal(" Stacktrace:")));
430431
stmts.add(JassIm.ImSet(trace, JassIm.ImVarAccess(traceI), JassIm.ImVarAccess(stackSize)));
431432
stmts.add(JassIm.ImSet(trace, JassIm.ImVarAccess(traceLimit), JassIm.ImIntVal(0)));
432433
ImStmts loopBody = JassIm.ImStmts();
@@ -447,6 +448,12 @@ private void rewriteErrorStatements(final Multimap<ImFunction, ImGetStackTrace>
447448
JassIm.ImOperatorCall(WurstOperator.PLUS, JassIm.ImExprs(JassIm.ImStringVal("\n "),
448449
JassIm.ImVarArrayAccess(trace, stack, JassIm.ImExprs(JassIm.ImVarAccess(traceI)))))))));
449450

451+
// Make empty traces explicit instead of returning an empty string.
452+
stmts.add(JassIm.ImIf(trace, JassIm.ImOperatorCall(WurstOperator.EQ,
453+
JassIm.ImExprs(JassIm.ImVarAccess(traceStr), JassIm.ImStringVal(" Stacktrace:"))),
454+
JassIm.ImStmts(JassIm.ImSet(trace, JassIm.ImVarAccess(traceStr), JassIm.ImStringVal(" Stacktrace: <none>"))),
455+
JassIm.ImStmts()));
456+
450457
s.replaceBy(JassIm.ImStatementExpr(stmts, JassIm.ImVarAccess(traceStr)));
451458
}
452459
}

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package tests.wurstscript.tests;
22

3+
import com.google.common.base.Charsets;
4+
import com.google.common.io.Files;
35
import de.peeeq.wurstio.jassinterpreter.InterpreterException;
46
import de.peeeq.wurstscript.ast.ClassDef;
57
import de.peeeq.wurstscript.ast.FuncDef;
@@ -1070,6 +1072,18 @@ public void testStacktrace() {
10701072
" if foo(bar(1), bar(2))",
10711073
" testSuccess()"
10721074
);
1075+
1076+
try {
1077+
String jass = Files.toString(
1078+
new File(TEST_OUTPUT_PATH + "BugTests_testStacktrace_stacktraceinlopt.j"),
1079+
Charsets.UTF_8
1080+
);
1081+
Assert.assertTrue(jass.contains("wurst_stack_depth"));
1082+
Assert.assertTrue(jass.contains("wurst_stack"));
1083+
Assert.assertFalse(jass.contains("return \"\""));
1084+
} catch (IOException e) {
1085+
throw new RuntimeException(e);
1086+
}
10731087
}
10741088

10751089
@Test
@@ -1450,6 +1464,37 @@ public void executeFuncWithStackTrace() {
14501464
);
14511465
}
14521466

1467+
@Test
1468+
public void executeFuncBridgeKeepsCallerStackFrames() {
1469+
test().executeProg(false).executeTests(false).lines(
1470+
"package Test",
1471+
"@extern native ExecuteFunc(string f)",
1472+
"function getStackTraceString() returns string",
1473+
" return \"\"",
1474+
"function foo()",
1475+
" getStackTraceString()",
1476+
"function caller()",
1477+
" getStackTraceString()",
1478+
" ExecuteFunc(\"foo\")",
1479+
" getStackTraceString()",
1480+
"init",
1481+
" caller()"
1482+
);
1483+
1484+
try {
1485+
String jass = Files.toString(
1486+
new File(TEST_OUTPUT_PATH + "BugTests_executeFuncBridgeKeepsCallerStackFrames_stacktraceinlopt.j"),
1487+
Charsets.UTF_8
1488+
);
1489+
Assert.assertTrue(jass.contains("function bridge_foo"));
1490+
Assert.assertFalse(jass.contains("bridge_oldStackDepth"));
1491+
Assert.assertFalse(jass.contains("set wurst_stack_depth = 0"));
1492+
Assert.assertTrue(jass.contains("via ExecuteFunc"));
1493+
} catch (IOException e) {
1494+
throw new RuntimeException(e);
1495+
}
1496+
}
1497+
14531498
@Test
14541499
public void agentTypeComparisonsWurst() {
14551500
testAssertErrorsLinesWithStdLib(true, "Cannot compare types sound with rect",

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,6 +2091,49 @@ public void fullArrayListTest() throws IOException {
20912091
assertEquals(count, 2);
20922092

20932093
assertFalse(compiled.contains("ArrayList_nextFreeIndex_"));
2094+
// In checked mode, virtual method calls should go through dispatch.
2095+
assertTrue(compiled.contains("dispatch_ArrayList"));
2096+
}
2097+
2098+
@Test
2099+
public void fullArrayListTestUncheckedDispatch() throws IOException {
2100+
test().withStdLib().uncheckedDispatch().executeProg().executeTests().file(new File(TEST_DIR + "arrayList.wurst"));
2101+
2102+
String compiled = Files.toString(
2103+
new File(TEST_OUTPUT_PATH + "GenericsWithTypeclassesTests_fullArrayListTestUncheckedDispatch_no_opts.jim"),
2104+
Charsets.UTF_8);
2105+
2106+
// In unchecked dispatch mode, monomorphic ArrayList<T>.get should be lowered directly.
2107+
assertTrue(compiled.contains("ArrayList_get"));
2108+
assertFalse(compiled.contains("dispatch_ArrayList"));
2109+
}
2110+
2111+
@Test
2112+
public void fullReactiveGenericDispatchTest() throws IOException {
2113+
test().withStdLib().executeProg().executeTests().file(new File(TEST_DIR + "reactiveGenericsDispatch.wurst"));
2114+
2115+
String compiled = Files.toString(
2116+
new File(TEST_OUTPUT_PATH + "GenericsWithTypeclassesTests_fullReactiveGenericDispatchTest_no_opts.jim"),
2117+
Charsets.UTF_8);
2118+
2119+
// In checked mode, polymorphic interface calls are dispatched and guarded.
2120+
assertTrue(compiled.contains("dispatch_ReactiveSource_ReactiveSource_subscribe"));
2121+
assertTrue(compiled.contains("Nullpointer exception when calling ReactiveSource.subscribe"));
2122+
assertTrue(compiled.contains("Called ReactiveSource.subscribe on invalid object."));
2123+
}
2124+
2125+
@Test
2126+
public void fullReactiveGenericDispatchTestUncheckedDispatch() throws IOException {
2127+
test().withStdLib().uncheckedDispatch().executeProg().executeTests().file(new File(TEST_DIR + "reactiveGenericsDispatch.wurst"));
2128+
2129+
String compiled = Files.toString(
2130+
new File(TEST_OUTPUT_PATH + "GenericsWithTypeclassesTests_fullReactiveGenericDispatchTestUncheckedDispatch_no_opts.jim"),
2131+
Charsets.UTF_8);
2132+
2133+
// ReactiveSource is polymorphic, so dispatch remains, but safety checks should be removed.
2134+
assertTrue(compiled.contains("dispatch_ReactiveSource_ReactiveSource_subscribe"));
2135+
assertFalse(compiled.contains("Nullpointer exception when calling ReactiveSource.subscribe"));
2136+
assertFalse(compiled.contains("Called ReactiveSource.subscribe on invalid object."));
20942137
}
20952138

20962139
@Test

de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package tests.wurstscript.tests;
22

3+
import config.WurstProjectConfigData;
34
import de.peeeq.wurstio.languageserver.BufferManager;
45
import de.peeeq.wurstio.languageserver.ModelManagerImpl;
56
import de.peeeq.wurstio.languageserver.WFile;
@@ -85,11 +86,69 @@ public void jhcrPipelineRenamesOutputScript() throws Exception {
8586
assertEquals(renamed.getName(), MapRequest.BUILD_JHCR_SCRIPT_NAME);
8687
}
8788

89+
@Test
90+
public void hotReloadDoesNotRequireSourceMap() throws Exception {
91+
File projectFolder = new File("./temp/testProject_hotreload_nomap/");
92+
File wurstFolder = new File(projectFolder, "wurst");
93+
newCleanFolder(wurstFolder);
94+
95+
File war3mapJ = new File(wurstFolder, "war3map.j");
96+
Files.writeString(war3mapJ.toPath(), "function main takes nothing returns nothing\nendfunction");
97+
98+
WurstLanguageServer langServer = new WurstLanguageServer();
99+
HotReloadNoMapRequest request = new HotReloadNoMapRequest(
100+
langServer,
101+
Optional.empty(),
102+
List.of("-hotreload"),
103+
WFile.create(projectFolder)
104+
);
105+
106+
MapRequest.CompilationResult result = request.compileScriptForTest(
107+
new ModelManagerImpl(projectFolder, new BufferManager()),
108+
new WurstGuiLogger(),
109+
Optional.empty(),
110+
new WurstProjectConfigData()
111+
);
112+
113+
assertEquals(result.script.getCanonicalFile(), war3mapJ.getCanonicalFile());
114+
}
115+
88116
private void newCleanFolder(File f) throws Exception {
89117
FileUtils.deleteRecursively(f);
90118
Files.createDirectories(f.toPath());
91119
}
92120

121+
private static final class HotReloadNoMapRequest extends MapRequest {
122+
private HotReloadNoMapRequest(WurstLanguageServer langServer, Optional<File> map, List<String> compileArgs,
123+
WFile workspaceRoot) {
124+
super(langServer, map, compileArgs, workspaceRoot, Optional.empty(), Optional.empty());
125+
}
126+
127+
@Override
128+
public Object execute(de.peeeq.wurstio.languageserver.ModelManager modelManager) {
129+
return null;
130+
}
131+
132+
@Override
133+
protected File ensureCachedMap(WurstGui gui) {
134+
throw new AssertionError("ensureCachedMap should not be called for hotreload without a map.");
135+
}
136+
137+
@Override
138+
protected File compileScript(WurstGui gui, de.peeeq.wurstio.languageserver.ModelManager modelManager,
139+
List<String> compileArgs, Optional<File> mapCopy,
140+
WurstProjectConfigData projectConfigData, boolean isProd, File scriptFile) {
141+
return scriptFile;
142+
}
143+
144+
private CompilationResult compileScriptForTest(de.peeeq.wurstio.languageserver.ModelManager modelManager,
145+
WurstGui gui,
146+
Optional<File> testMap,
147+
WurstProjectConfigData projectConfigData) throws Exception {
148+
return compileScript(modelManager, gui, testMap, projectConfigData, getBuildDir(), false);
149+
}
150+
}
151+
93152
private static final class TestMapRequest extends MapRequest {
94153
private final Map<File, byte[]> scriptByMap;
95154
private File lastExtractedMap;

0 commit comments

Comments
 (0)