diff --git a/.circleci/config.yml b/.circleci/config.yml index 85e30c85a..c845fbf96 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,8 +17,7 @@ jobs: working_directory: ~/repo/de.peeeq.wurstscript environment: - # Customize the JVM maximum heap limit - JVM_OPTS: -Xmx3200m + GRADLE_OPTS: -Dorg.gradle.parallel=false -Dorg.gradle.workers.max=2 TERM: dumb steps: @@ -28,20 +27,18 @@ jobs: # Download and cache dependencies - restore_cache: keys: - - v1-dependencies-{{ checksum "build.gradle" }} + - v2-gradle-{{ checksum "build.gradle" }}-{{ checksum "gradle.properties" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }} # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: ./gradlew dependencies + - v2-gradle- - save_cache: paths: - ~/.gradle - key: v1-dependencies-{{ checksum "build.gradle" }} - - # run tests - - run: ./gradlew test --info + key: v2-gradle-{{ checksum "build.gradle" }}-{{ checksum "gradle.properties" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }} - # report tests results - - run: ./gradlew jacocoTestReport coveralls + # run tests and coverage in one invocation to avoid duplicate config/startup cost + - run: + name: Run tests and coverage + command: ./gradlew --no-daemon --stacktrace test jacocoTestReport coveralls + no_output_timeout: 30m diff --git a/de.peeeq.wurstscript/gradle.properties b/de.peeeq.wurstscript/gradle.properties index 1c50ec12a..e2beb559e 100644 --- a/de.peeeq.wurstscript/gradle.properties +++ b/de.peeeq.wurstscript/gradle.properties @@ -3,4 +3,5 @@ org.gradle.configuration-cache=true org.gradle.parallel=true org.gradle.daemon=true org.gradle.java.installations.auto-download=true -org.gradle.java.installations.auto-detect=true \ No newline at end of file +org.gradle.java.installations.auto-detect=true +org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 diff --git a/de.peeeq.wurstscript/parserspec/wurstscript.parseq b/de.peeeq.wurstscript/parserspec/wurstscript.parseq index 6f84941a7..9e9e492b6 100644 --- a/de.peeeq.wurstscript/parserspec/wurstscript.parseq +++ b/de.peeeq.wurstscript/parserspec/wurstscript.parseq @@ -147,6 +147,7 @@ ControlflowStatement = CompoundStatement | StmtReturn(@ignoreForEquality de.peeeq.wurstscript.parser.WPos source, OptExpr returnedObj) | StmtExitwhen(@ignoreForEquality de.peeeq.wurstscript.parser.WPos source, Expr cond) + | StmtContinue(de.peeeq.wurstscript.parser.WPos source) CompoundStatement = StmtIf(@ignoreForEquality de.peeeq.wurstscript.parser.WPos source, Expr cond, WStatements thenBlock, WStatements elseBlock, boolean hasElse) @@ -1073,4 +1074,4 @@ Annotation.getAnnotationType() Annotation.getAnnotationMessage() returns String - implemented by de.peeeq.wurstscript.attributes.Annotations.annotationMessage \ No newline at end of file + implemented by de.peeeq.wurstscript.attributes.Annotations.annotationMessage diff --git a/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 b/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 index e3681dec8..bffeeb67d 100644 --- a/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 +++ b/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 @@ -260,6 +260,7 @@ statement: | stmtSet (externalLambda|NL) | stmtReturn (externalLambda|NL) | stmtBreak NL + | stmtContinue NL | stmtSkip NL | expr (externalLambda|NL) | stmtIf @@ -435,6 +436,7 @@ forIteratorLoop: stmtBreak:'break'; +stmtContinue:'continue'; stmtSkip:'skip'; @@ -461,6 +463,7 @@ WHILE: 'while'; FOR: 'for'; IN: 'in'; BREAK: 'break'; +CONTINUE: 'continue'; NEW: 'new'; NULL: 'null'; PACKAGE: 'package'; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index 5c3e4332f..d731a570d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -28,6 +28,7 @@ import de.peeeq.wurstscript.translation.imtojass.ImAttrType; import de.peeeq.wurstscript.translation.imtojass.ImToJassTranslator; import de.peeeq.wurstscript.translation.imtranslation.*; +import de.peeeq.wurstscript.translation.lua.translation.RemoveGarbage; import de.peeeq.wurstscript.translation.lua.translation.LuaTranslator; import de.peeeq.wurstscript.types.TypesHelper; import de.peeeq.wurstscript.utils.LineOffsets; @@ -937,7 +938,12 @@ public LuaCompilationUnit transformProgToLua() { printDebugImProg("./test-output/lua/im " + stage++ + "_afteroptimize.im"); timeTaker.endPhase(); } - beginPhase(13, "translate to lua"); + beginPhase(13, "lua remove garbage"); + RemoveGarbage.removeGarbage(imProg); + imProg.flatten(imTranslator); + timeTaker.endPhase(); + + beginPhase(14, "translate to lua"); LuaTranslator luaTranslator = new LuaTranslator(imProg, imTranslator); LuaCompilationUnit luaCode = luaTranslator.translate(); ImAttrType.setWurstClassType(TypesHelper.imInt()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/LanguageWorker.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/LanguageWorker.java index 3fb845696..d56813e25 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/LanguageWorker.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/LanguageWorker.java @@ -213,13 +213,8 @@ private Workitem getNextWorkItem() { FileReconcile fr = (FileReconcile) change; affected = modelManager.syncCompilationUnitContent(fr.getFilename(), fr.getContents()); } else if (change instanceof FileSystemUpdated || change instanceof FileDeleted) { - // Dependency roots may have changed (e.g. grill install), refresh and sync incrementally. - modelManager.refreshDependencies(); - if (change instanceof FileDeleted) { - affected = modelManager.removeCompilationUnit(change.getFilename()); - } else { - affected = modelManager.syncCompilationUnit(change.getFilename()); - } + // Dependency roots may have changed (e.g. grill install), sync full dependency state. + affected = modelManager.syncDependencyCompilationUnits(); } else { // Editor-triggered updates (save/close) use the normal incremental path. affected = modelManager.syncCompilationUnit(change.getFilename()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManager.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManager.java index a582a5800..536847b34 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManager.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManager.java @@ -34,6 +34,12 @@ public interface ModelManager { */ void refreshDependencies(); + /** + * refresh and synchronize all dependency compilation units. + * This handles dependency delete/replace/move/rename scenarios robustly. + */ + Changes syncDependencyCompilationUnits(); + Changes syncCompilationUnit(WFile changedFilePath); Changes syncCompilationUnitContent(WFile filename, String contents); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java index 0520bd019..adf855562 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java @@ -19,6 +19,7 @@ import java.io.*; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.*; import java.util.function.Consumer; @@ -184,6 +185,39 @@ public void refreshDependencies() { readDependencies(); } + @Override + public synchronized Changes syncDependencyCompilationUnits() { + readDependencies(); + + WurstModel model2 = model; + if (model2 == null) { + return Changes.empty(); + } + + Set expectedDependencyFiles = getDependencyWurstFiles().stream() + .map(WFile::create) + .collect(Collectors.toSet()); + + List loadedDependencyFiles = model2.stream() + .map(this::wFile) + .filter(this::isUnderDependenciesFolder) + .collect(Collectors.toList()); + + Changes changes = Changes.empty(); + + for (WFile loadedFile : loadedDependencyFiles) { + if (!expectedDependencyFiles.contains(loadedFile)) { + changes = changes.mergeWith(removeCompilationUnit(loadedFile)); + } + } + + for (WFile dependencyFile : expectedDependencyFiles) { + changes = changes.mergeWith(syncCompilationUnit(dependencyFile)); + } + + return changes; + } + private String getCanonicalPath(File f) { try { return f.getCanonicalPath(); @@ -861,6 +895,18 @@ private boolean isAlreadyLoaded(WFile file) { return false; } + private boolean isUnderDependenciesFolder(WFile file) { + try { + Path filePath = file.getPath().toAbsolutePath().normalize(); + Path dependencyRoot = Paths.get(projectPath.getAbsolutePath(), "_build", "dependencies") + .toAbsolutePath() + .normalize(); + return filePath.startsWith(dependencyRoot) && Utils.isWurstFile(filePath.toString()); + } catch (FileNotFoundException e) { + return false; + } + } + public File getProjectPath() { return projectPath; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/HoverInfo.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/HoverInfo.java index 45ab7c0bc..af453b3fd 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/HoverInfo.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/HoverInfo.java @@ -422,6 +422,11 @@ public List> case_StmtExitwhen(StmtExitwhen stmtExi return string("exitwhen: exits the current loop when the condition is true."); } + @Override + public List> case_StmtContinue(StmtContinue stmtContinue) { + return string("continue: skips the rest of the current loop iteration."); + } + @Override public List> case_ConstructorDef(ConstructorDef constr) { return description(constr); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java index 539941668..434a75da8 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/MapRequest.java @@ -27,6 +27,7 @@ import de.peeeq.wurstscript.jassprinter.JassPrinter; import de.peeeq.wurstscript.luaAst.LuaCompilationUnit; import de.peeeq.wurstscript.parser.WPos; +import de.peeeq.wurstscript.translation.lua.translation.LuaTranslator; import de.peeeq.wurstscript.utils.LineOffsets; import de.peeeq.wurstscript.utils.Utils; import net.moonlightflower.wc3libs.bin.app.W3I; @@ -167,6 +168,7 @@ protected File compileMap(File projectFolder, WurstGui gui, Optional mapCo luaCode.get().print(sb, 0); String compiledMapScript = sb.toString(); + LuaTranslator.assertNoLeakedHashtableNativeCalls(compiledMapScript); File buildDir = getBuildDir(); File outFile = new File(buildDir, BUILD_COMPILED_LUA_NAME); Files.write(compiledMapScript.getBytes(Charsets.UTF_8), outFile); @@ -418,8 +420,12 @@ private String resolveCachedMapFileName() { if (!cachedMapFileName.isEmpty()) { return cachedMapFileName; } + return resolveCachedMapFileName(runArgs.isLua()); + } + + private String resolveCachedMapFileName(boolean luaMode) { if (!map.isPresent()) { - return "cached_map.w3x"; + return luaMode ? "cached_map_lua.w3x" : "cached_map_jass.w3x"; } File inputMap = map.get(); String inputName = inputMap.getName(); @@ -428,7 +434,8 @@ private String resolveCachedMapFileName() { // Keep only filesystem-safe characters and avoid collisions for same basename from different folders. String safeBase = baseName.replaceAll("[^a-zA-Z0-9._-]", "_"); String pathHash = Integer.toUnsignedString(inputMap.getAbsolutePath().hashCode(), 36); - return safeBase + "_" + pathHash + "_cached.w3x"; + String modeSuffix = luaMode ? "lua" : "jass"; + return safeBase + "_" + pathHash + "_" + modeSuffix + "_cached.w3x"; } protected File ensureWritableTargetFile(File targetFile, String dialogTitle, String lockMessage, @@ -498,6 +505,7 @@ private boolean isLocked(File targetMap) { */ protected File ensureCachedMap(WurstGui gui) throws IOException { File cachedMap = getCachedMapFile(); + cleanupOppositeModeCacheAndOutputs(); if (!map.isPresent()) { throw new RequestFailedException(MessageType.Error, "No source map provided"); @@ -515,6 +523,29 @@ protected File ensureCachedMap(WurstGui gui) throws IOException { return cachedMap; } + private void cleanupOppositeModeCacheAndOutputs() { + if (cachedMapFileName.isEmpty()) { + File cacheDir = new File(getBuildDir(), "cache"); + String oppositeModeCacheName = resolveCachedMapFileName(!runArgs.isLua()); + java.nio.file.Path oppositeModeCache = new File(cacheDir, oppositeModeCacheName).toPath(); + try { + java.nio.file.Files.deleteIfExists(oppositeModeCache); + } catch (IOException e) { + WLogger.warning("Could not delete opposite-mode cached map: " + oppositeModeCache + " (" + e.getMessage() + ")"); + } + } + + File buildDir = getBuildDir(); + File oppositeCompiledOutput = runArgs.isLua() + ? new File(buildDir, BUILD_COMPILED_JASS_NAME) + : new File(buildDir, BUILD_COMPILED_LUA_NAME); + try { + java.nio.file.Files.deleteIfExists(oppositeCompiledOutput.toPath()); + } catch (IOException e) { + WLogger.warning("Could not delete opposite-mode compiled output: " + oppositeCompiledOutput + " (" + e.getMessage() + ")"); + } + } + protected CompilationResult compileScript(ModelManager modelManager, WurstGui gui, Optional testMap, WurstProjectConfigData projectConfigData, File buildDir, boolean isProd) throws Exception { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstKeywords.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstKeywords.java index c4c63256f..182dbc91d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstKeywords.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WurstKeywords.java @@ -1,7 +1,7 @@ package de.peeeq.wurstscript; public class WurstKeywords { - public static final String[] KEYWORDS = new String[]{"class", "return", "if", "else", "while", "for", "in", "break", "new", "null", + public static final String[] KEYWORDS = new String[]{"class", "return", "if", "else", "while", "for", "in", "break", "continue", "new", "null", "package", "endpackage", "function", "returns", "public", "private", "protected", "import", "initlater", "native", "nativetype", "extends", "interface", "implements", "module", "use", "abstract", "static", "thistype", "override", "immutable", "it", "array", "and", "or", "not", "this", "construct", "ondestroy", "destroy", "type", "constant", "endfunction", "nothing", "init", "castTo", diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrClosureCapturedVariables.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrClosureCapturedVariables.java index 764b20af0..eb21d2184 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrClosureCapturedVariables.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrClosureCapturedVariables.java @@ -62,7 +62,8 @@ private static void collect(Builder result, ExprClosure closure private static boolean isLocalVariable(NameDef def) { return def instanceof LocalVarDef - || def instanceof WParameter && !(def.getParent().getParent() instanceof TupleDef); + || def instanceof WParameter && !(def.getParent().getParent() instanceof TupleDef) + || def instanceof WShortParameter; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java index 880fd82b7..c57ea9aa4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java @@ -5,6 +5,7 @@ import de.peeeq.wurstscript.types.FunctionSignature; import de.peeeq.wurstscript.types.VariableBinding; import de.peeeq.wurstscript.types.WurstType; +import de.peeeq.wurstscript.types.WurstTypeCode; import de.peeeq.wurstscript.types.WurstTypeUnknown; import de.peeeq.wurstscript.utils.Utils; import org.jetbrains.annotations.NotNull; @@ -57,9 +58,41 @@ public static FunctionSignature calculate(StmtCall fc) { fc.addError("Cannot infer type for type parameter " + mapping.printUnboundTypeVars()); } + checkCodeClosureCaptures(fc, sig); + return sig; } + private static void checkCodeClosureCaptures(StmtCall fc, FunctionSignature sig) { + if (!sig.isValidParameterNumber(fc.getArgs().size())) { + return; + } + for (int i = 0; i < fc.getArgs().size(); i++) { + Expr arg = fc.getArgs().get(i); + if (!(arg instanceof ExprClosure)) { + continue; + } + if (!(sig.getParamType(i) instanceof WurstTypeCode)) { + continue; + } + ExprClosure closure = (ExprClosure) arg; + if (!closure.attrCapturedVariables().isEmpty()) { + String codeLambdaContext = codeLambdaContext(fc); + closure.attrCapturedVariables().entries().forEach(entry -> + entry.getKey().addError("Cannot capture local variable '" + entry.getValue().getName() + + "' in anonymous function" + codeLambdaContext + ". This is only possible with closures.")); + } + } + } + + private static String codeLambdaContext(StmtCall stmtCall) { + String funcName = stmtCall instanceof FunctionCall fc ? fc.getFuncName() : ""; + if (stmtCall instanceof ExprMemberMethod) { + return " passed as code to ." + funcName + "() ->"; + } + return " passed as code to " + funcName + "() ->"; + } + private static FunctionSignature filterSigs( Collection sigs, List argTypes, StmtCall location) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java index 2d454d0d1..584dbcb8d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java @@ -85,8 +85,14 @@ private static ImmutableCollection findBestSignature(StmtCall fc.getErrorHandler().sendError(c); } } - - return ImmutableList.copyOf(inferred); + ImmutableList.Builder result = ImmutableList.builder(); + for (int i = 0; i < n; i++) { + var r = sigs.get(i).tryMatchAgainstArgs(argTypes, argsNode, fc); + if (r.getBadness() == bestBad) { + result.add(inferred[i]); + } + } + return result.build(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/DescriptionHtml.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/DescriptionHtml.java index 0f83f666f..6ffd24e10 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/DescriptionHtml.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/DescriptionHtml.java @@ -326,6 +326,10 @@ public static String description(StmtSkip stmtSkip) { return "The skip statement does nothing. Just skip this line."; } + public static String description(StmtContinue stmtContinue) { + return "continue: Skips the rest of the current loop iteration."; + } + public static String description(StmtWhile stmtWhile) { return "While Statement: Repeat while the condition is true."; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/Flow.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/Flow.java index 3a33a9782..4a69c22a4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/Flow.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/Flow.java @@ -59,6 +59,17 @@ public static List getNext(StmtExitwhen s) { return next; } + public static List getNext(StmtContinue s) { + LoopStatement loop = getParentLoopStatement(s); + if (loop == null) { + s.addError("Continue statements must be used inside a loop."); + return Collections.emptyList(); + } + List next = loop.attrAfterBodyStatements(); + setPrevios(s, next); + return next; + } + private static boolean isConstantBool(Expr cond, boolean value) { return cond instanceof ExprBoolVal && ((ExprBoolVal) cond).getValB() == value; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/InitOrder.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/InitOrder.java index 075f523b9..9e37a6217 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/InitOrder.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/InitOrder.java @@ -7,6 +7,7 @@ import de.peeeq.wurstscript.ast.WImport; import de.peeeq.wurstscript.ast.WImports; import de.peeeq.wurstscript.ast.WPackage; +import de.peeeq.wurstscript.ast.WurstModel; import java.util.*; @@ -19,7 +20,8 @@ public static ImmutableList initDependencies(WPackage p) { packages.addAll(p.attrImportedPackagesTransitive()); // add config package if it exists: - WPackage configPackage = p.getModel().attrConfigOverridePackages().get(p); + WurstModel model = safeModel(p); + WPackage configPackage = model == null ? null : model.attrConfigOverridePackages().get(p); if (configPackage != null) { packages.add(configPackage); } @@ -70,8 +72,11 @@ private static String getCyclicDependencyString(List callStack, WPacka StringBuilder msg = new StringBuilder(); Map configuredPackage = new HashMap<>(); if (considerConfig) { - for (WPackage configured : imported.getModel().attrConfigOverridePackages().keySet()) { - configuredPackage.put(imported.getModel().attrConfigOverridePackages().get(configured), configured); + WurstModel model = safeModel(imported); + if (model != null) { + for (WPackage configured : model.attrConfigOverridePackages().keySet()) { + configuredPackage.put(model.attrConfigOverridePackages().get(configured), configured); + } } } for (WPackage p : callStack) { @@ -152,7 +157,8 @@ private static void addCollectImportedPackage(List callStack, WPackage // add imports of configured package to config package // that way cyclic dependencies are checked for the config package and errors will be reported for the config package if (considerConfig) { - WPackage configPackage = p.getModel().attrConfigOverridePackages().get(imported); + WurstModel model = safeModel(p); + WPackage configPackage = model == null ? null : model.attrConfigOverridePackages().get(imported); if (configPackage != null && configPackage != p) { if (configPackage == callStack.get(0)) { reportCyclicDependency(callStack, configPackage, reportedErrors, useWarnings); @@ -168,6 +174,10 @@ private static void addCollectImportedPackage(List callStack, WPackage } private static void collectImportedPackages(List callStack, WPackage p, Collection result, Set reportedErrors, boolean considerConfig, boolean useWarnings) { + if (safeModel(p) == null) { + // Detached packages can occur transiently in language-server workflows; ignore them for init-order analysis. + return; + } callStack.add(p); addCollectImportedPackage(callStack, p, result, p.getImports(), reportedErrors, considerConfig, useWarnings); /* @@ -178,7 +188,8 @@ private static void collectImportedPackages(List callStack, WPackage p even though the configured package will be initialized after the config package. */ if (considerConfig) { - for (Map.Entry e : p.getModel().attrConfigOverridePackages().entrySet()) { + WurstModel model = safeModel(p); + if (model != null) for (Map.Entry e : model.attrConfigOverridePackages().entrySet()) { if (e.getValue().equals(p)) { addCollectImportedPackage(callStack, e.getKey(), result, e.getKey().getImports(), reportedErrors, considerConfig, useWarnings); } @@ -187,4 +198,12 @@ private static void collectImportedPackages(List callStack, WPackage p callStack.remove(callStack.size() - 1); } + private static WurstModel safeModel(WPackage p) { + try { + return p.getModel(); + } catch (Error ignored) { + return null; + } + } + } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java index 8e908c913..017c84995 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java @@ -1146,6 +1146,12 @@ public static void prettyPrint(StmtSkip e, Spacer spacer, StringBuilder sb, int sb.append("\n"); } + public static void prettyPrint(StmtContinue e, Spacer spacer, StringBuilder sb, int indent) { + printIndent(sb, indent); + sb.append("continue"); + sb.append("\n"); + } + public static void prettyPrint(StmtWhile e, Spacer spacer, StringBuilder sb, int indent) { printIndent(sb, indent); sb.append("while"); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java index bbbd03425..3b20d5377 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java @@ -747,6 +747,8 @@ private WStatement transformStatement2(StatementContext s) { } else if (s.stmtBreak() != null) { return Ast .StmtExitwhen(source(s), Ast.ExprBoolVal(source(s), true)); + } else if (s.stmtContinue() != null) { + return Ast.StmtContinue(source(s)); } else if (s.stmtSkip() != null) { return Ast.StmtSkip(source(s)); } else if (s.stmtSwitch() != null) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java index 9838ebd59..07e4a7437 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClosureTranslator.java @@ -10,6 +10,8 @@ import de.peeeq.wurstscript.translation.imtojass.TypeRewriteMatcher; import de.peeeq.wurstscript.translation.imtojass.TypeRewriter; import de.peeeq.wurstscript.types.*; +import de.peeeq.wurstscript.parser.WPos; +import de.peeeq.wurstscript.utils.Utils; import java.util.Collections; import java.util.LinkedHashMap; @@ -123,7 +125,9 @@ private void verifyTranslatedAnonfunc(ImExpr translated) { public void visit(ImVarAccess va) { super.visit(va); if (isLocalToOtherFunc(va.getVar())) { - throw new CompileError(va.attrTrace().attrSource(), "Anonymous functions used as 'code' cannot capture variables. Captured " + va.getVar().getName()); + throw new CompileError(bestCaptureErrorPos(va), + "Anonymous functions used as 'code' cannot capture variables. Captured " + + va.getVar().getName() + closureDebugContext()); } } @@ -131,12 +135,49 @@ public void visit(ImVarAccess va) { public void visit(ImSet s) { super.visit(s); if (isLocalToOtherFunc(s.getLeft())) { - throw new CompileError(s.attrTrace().attrSource(), "Anonymous functions used as 'code' cannot capture variables. Captured " + s.getLeft()); + throw new CompileError(bestCaptureErrorPos(s), + "Anonymous functions used as 'code' cannot capture variables. Captured " + + s.getLeft() + closureDebugContext()); } } }); } + private String closureDebugContext() { + String pos = ""; + WPos p = e.attrErrorPos(); + if (isUsableSource(p)) { + pos = p.printShort(); + } + String nearestFunc = e.attrNearestFuncDef() == null ? "" : e.attrNearestFuncDef().getName(); + String nearestPkg = e.attrNearestPackage() == null ? "" : Utils.printElement(e.attrNearestPackage()); + return " [closure=" + pos + + ", expectedType=" + e.attrExpectedTypAfterOverloading() + + ", closureType=" + e.attrTyp() + + ", nearestFunc=" + nearestFunc + + ", nearestPackage=" + nearestPkg + "]"; + } + + private WPos bestCaptureErrorPos(ImStmt s) { + WPos src = s.attrTrace().attrSource(); + if (isUsableSource(src)) { + return src; + } + return e.attrErrorPos(); + } + + private WPos bestCaptureErrorPos(ImVarAccess va) { + WPos src = va.attrTrace().attrSource(); + if (isUsableSource(src)) { + return src; + } + return e.attrErrorPos(); + } + + private boolean isUsableSource(WPos pos) { + return pos != null && pos.getLine() > 0 && pos.getFile() != null && !pos.getFile().isEmpty(); + } + private ImClass createClass() { ImClassType superClass = getSuperClass(); @@ -247,6 +288,7 @@ public ImType case_ImTypeVarRef(ImTypeVarRef t) { */ private void transformTranslated(ImExpr t) { final List vas = Lists.newArrayList(); + final List thisVarAccessesToRewrite = Lists.newArrayList(); final List receiversToRewrite = Lists.newArrayList(); ImVar closureThisVar = tr.getThisVar(e); @@ -256,6 +298,14 @@ public void visit(ImVarAccess va) { super.visit(va); if (isLocalToOtherFunc(va.getVar())) { vas.add(va); + return; + } + if (capturedThisField != null && va.getVar() == closureThisVar) { + // Explicit `this` used inside a closure (e.g. "var cur = this") + // must refer to captured outer this, not the synthetic closure instance. + if (!isReceiverInMemberAccess(va)) { + thisVarAccessesToRewrite.add(va); + } } } @@ -277,11 +327,22 @@ public void visit(ImMemberAccess ma) { va.replaceBy(JassIm.ImMemberAccess(e, closureThis(), JassIm.ImTypeArguments(), v, JassIm.ImExprs())); } + for (ImVarAccess va : thisVarAccessesToRewrite) { + va.replaceBy(JassIm.ImMemberAccess(e, closureThis(), JassIm.ImTypeArguments(), capturedThisField, JassIm.ImExprs())); + } + for (ImMemberAccess ma : receiversToRewrite) { ma.setReceiver(JassIm.ImMemberAccess(e, closureThis(), JassIm.ImTypeArguments(), capturedThisField, JassIm.ImExprs())); } } + private boolean isReceiverInMemberAccess(ImVarAccess va) { + if (va.getParent() instanceof ImMemberAccess ma) { + return ma.getReceiver() == va; + } + return false; + } + private void captureEnclosingThis() { ImVar outerThis = getEnclosingThisVar(); if (outerThis != null) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java index f825187f7..db5870f25 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java @@ -97,6 +97,7 @@ public class ImTranslator { private final Map> capturedOwnerTypeVarsByStaticClass = new IdentityHashMap<>(); private final Deque> typeVarOverrideStack = new ArrayDeque<>(); + private final Deque continueFlagStack = new ArrayDeque<>(); private static boolean hasTypeVarNamed(ImTypeVars vars, String name) { for (ImTypeVar v : vars) { @@ -118,6 +119,18 @@ public void popTypeVarOverrides(Map m) { if (m != null && !m.isEmpty()) typeVarOverrideStack.pop(); } + public void pushContinueFlag(ImVar continueFlag) { + continueFlagStack.push(continueFlag); + } + + public void popContinueFlag() { + continueFlagStack.pop(); + } + + public @Nullable ImVar currentContinueFlag() { + return continueFlagStack.peek(); + } + public ImTranslator(WurstModel wurstProg, boolean isUnitTestMode, RunArgs runArgs) { this.wurstProg = wurstProg; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java index e3b16ef9c..24940f14d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/StmtTranslation.java @@ -52,6 +52,14 @@ public static ImStmt translate(StmtExitwhen s, ImTranslator t, ImFunction f) { return ImExitwhen(s, s.getCond().imTranslateExpr(t, f)); } + public static ImStmt translate(StmtContinue s, ImTranslator t, ImFunction f) { + ImVar continueFlag = t.currentContinueFlag(); + if (continueFlag == null) { + throw new CompileError(s.getSource(), "Continue is not allowed outside of loop statements."); + } + return ImSet(s, ImVarAccess(continueFlag), JassIm.ImBoolVal(true)); + } + public static ImStmt translate(StmtForFrom s, ImTranslator t, ImFunction f) { Expr iterationTarget = s.getIn(); @@ -93,8 +101,7 @@ public static ImStmt translate(StmtForFrom s, ImTranslator t, ImFunction f) { ImExpr nextCallWrapped = ExprTranslation.wrapTranslation(s, t, nextCall, nextReturn, loopVarType); imBody.add(ImSet(s, ImVarAccess(t.getVarFor(s.getLoopVar())), nextCallWrapped)); - - imBody.addAll(t.translateStatements(f, s.getBody())); + imBody.addAll(translateLoopBody(t, f, s.getBody(), s)); result.add(ImLoop(s, imBody)); } @@ -199,7 +206,7 @@ public static ImStmt translate(StmtForIn forIn, ImTranslator t, ImFunction f) { imBody.add(ImSet(forIn, ImVarAccess(t.getVarFor(forIn.getLoopVar())), nextCallWrapped)); // loop body - imBody.addAll(t.translateStatements(f, forIn.getBody())); + imBody.addAll(translateLoopBody(t, f, forIn.getBody(), forIn)); // optional close()() Optional closeFunc = forIn.attrCloseFunc(); @@ -287,7 +294,7 @@ private static ImStmt case_StmtForRange(ImTranslator t, ImFunction f, LocalVarDe // exitwhen imLoopVar > toExpr imBody.add(ImExitwhen(trace, ImOperatorCall(opCompare, ImExprs(ImVarAccess(imLoopVar), toExpr)))); // loop body: - imBody.addAll(t.translateStatements(f, body)); + imBody.addAll(translateLoopBody(t, f, body, trace)); // set imLoopVar = imLoopVar + stepExpr imBody.add(ImSet(trace, ImVarAccess(imLoopVar), ImOperatorCall(opStep, ImExprs(ImVarAccess(imLoopVar), stepExpr)))); result.add(ImLoop(trace, imBody)); @@ -313,7 +320,7 @@ public static ImStmt translate(StmtIf s, ImTranslator t, ImFunction f) { public static ImStmt translate(StmtLoop s, ImTranslator t, ImFunction f) { - return ImLoop(s, ImStmts(t.translateStatements(f, s.getBody()))); + return ImLoop(s, ImStmts(translateLoopBody(t, f, s.getBody(), s))); } @@ -333,10 +340,81 @@ public static ImStmt translate(StmtWhile s, ImTranslator t, ImFunction f) { List body = Lists.newArrayList(); // exitwhen not while_condition body.add(ImExitwhen(s.getCond(), ImOperatorCall(WurstOperator.NOT, ImExprs(s.getCond().imTranslateExpr(t, f))))); - body.addAll(t.translateStatements(f, s.getBody())); + body.addAll(translateLoopBody(t, f, s.getBody(), s)); return ImLoop(s, ImStmts(body)); } + private static ImVar createContinueFlagVar(Element trace, ImFunction f) { + ImVar continueFlag = JassIm.ImVar(trace, TypesHelper.imBool(), "continueFlag_" + f.getLocals().size(), false); + f.getLocals().add(continueFlag); + return continueFlag; + } + + private static List translateLoopBodyWithContinue(ImTranslator t, ImFunction f, List body, ImVar continueFlag, Element trace) { + List guardedBody = Lists.newArrayList(); + guardedBody.add(ImSet(trace, ImVarAccess(continueFlag), JassIm.ImBoolVal(false))); + t.pushContinueFlag(continueFlag); + try { + for (WStatement s : body) { + ImStmt translated = s.imTranslateStmt(t, f); + ImExpr guard = ImOperatorCall(WurstOperator.NOT, ImExprs(ImVarAccess(continueFlag))); + guardedBody.add(ImIf(trace, guard, ImStmts(translated), ImStmts())); + } + } finally { + t.popContinueFlag(); + } + return guardedBody; + } + + private static List translateLoopBody(ImTranslator t, ImFunction f, List body, Element trace) { + if (!hasContinueForCurrentLoop(body)) { + return t.translateStatements(f, body); + } + ImVar continueFlag = createContinueFlagVar(trace, f); + return translateLoopBodyWithContinue(t, f, body, continueFlag, trace); + } + + private static boolean hasContinueForCurrentLoop(List body) { + for (WStatement statement : body) { + if (statement instanceof StmtContinue) { + return true; + } + if (statement instanceof StmtLoop + || statement instanceof StmtWhile + || statement instanceof StmtForIn + || statement instanceof StmtForFrom + || statement instanceof StmtForRangeUp + || statement instanceof StmtForRangeDown) { + // Continue inside nested loops should not trigger guarding for the outer loop. + continue; + } + if (statement instanceof WBlock && hasContinueForCurrentLoop(((WBlock) statement).getBody())) { + return true; + } + if (statement instanceof StmtIf) { + StmtIf stmtIf = (StmtIf) statement; + if (hasContinueForCurrentLoop(stmtIf.getThenBlock()) || hasContinueForCurrentLoop(stmtIf.getElseBlock())) { + return true; + } + } + if (statement instanceof SwitchStmt) { + SwitchStmt switchStmt = (SwitchStmt) statement; + for (SwitchCase switchCase : switchStmt.getCases()) { + if (hasContinueForCurrentLoop(switchCase.getStmts())) { + return true; + } + } + if (switchStmt.getSwitchDefault() instanceof SwitchDefaultCaseStatements) { + SwitchDefaultCaseStatements defaultCase = (SwitchDefaultCaseStatements) switchStmt.getSwitchDefault(); + if (hasContinueForCurrentLoop(defaultCase.getStmts())) { + return true; + } + } + } + } + return false; + } + public static ImStmt translate(StmtSkip s, ImTranslator translator, ImFunction f) { return ImHelper.nullExpr(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java index 5e34d47e6..916e7709c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/ExprTranslation.java @@ -7,12 +7,29 @@ import de.peeeq.wurstscript.types.TypesHelper; import java.util.Optional; +import java.util.Set; public class ExprTranslation { public static final String TYPE_ID = "__typeId__"; public static final String WURST_SUPERTYPES = "__wurst_supertypes"; private static final String WURST_ABORT_THREAD_SENTINEL = "__wurst_abort_thread"; + private static final Set LUA_HANDLE_TO_INDEX = Set.of( + "widgetToIndex", "unitToIndex", "destructableToIndex", "itemToIndex", "abilityToIndex", + "forceToIndex", "groupToIndex", "triggerToIndex", "triggeractionToIndex", "triggerconditionToIndex", + "timerToIndex", "locationToIndex", "regionToIndex", "rectToIndex", "soundToIndex", + "effectToIndex", "dialogToIndex", "buttonToIndex", "questToIndex", "questitemToIndex", + "leaderboardToIndex", "multiboardToIndex", "trackableToIndex", "lightningToIndex", + "ubersplatToIndex", "framehandleToIndex", "oskeytypeToIndex" + ); + private static final Set LUA_HANDLE_FROM_INDEX = Set.of( + "widgetFromIndex", "unitFromIndex", "destructableFromIndex", "itemFromIndex", "abilityFromIndex", + "forceFromIndex", "groupFromIndex", "triggerFromIndex", "triggeractionFromIndex", "triggerconditionFromIndex", + "timerFromIndex", "locationFromIndex", "regionFromIndex", "rectFromIndex", "soundFromIndex", + "effectFromIndex", "dialogFromIndex", "buttonFromIndex", "questFromIndex", "questitemFromIndex", + "leaderboardFromIndex", "multiboardFromIndex", "trackableFromIndex", "lightningFromIndex", + "ubersplatFromIndex", "framehandleFromIndex", "oskeytypeFromIndex" + ); public static LuaExpr translate(ImAlloc e, LuaTranslator tr) { ImClass c = e.getClazz().getClassDef(); @@ -36,29 +53,40 @@ public static LuaExpr translate(ImDealloc e, LuaTranslator tr) { public static LuaExpr translate(ImFuncRef e, LuaTranslator tr) { // return LuaAst.LuaExprFuncRef(tr.luaFunc.getFor(e.getFunc())); // alternative: use xpcall to get stacktraces (did not work) + boolean returnsValue = !(e.getFunc().getReturnType() instanceof ImVoid); LuaVariable dots = LuaAst.LuaVariable("...", LuaAst.LuaNoExpr()); - LuaVariable tempDots = LuaAst.LuaVariable("temp", LuaAst.LuaExprVarAccess(dots)); - LuaVariable tempRes = LuaAst.LuaVariable("tempRes", LuaAst.LuaExprNull()); - return LuaAst.LuaExprFunctionAbstraction(LuaAst.LuaParams(dots), - LuaAst.LuaStatements( - tempDots, - tempRes, - LuaAst.LuaExprFunctionCallByName("xpcall", - LuaAst.LuaExprlist( - LuaAst.LuaExprFunctionAbstraction( - LuaAst.LuaParams(), - LuaAst.LuaStatements( - LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(tempRes), - LuaAst.LuaExprFunctionCall(tr.luaFunc.getFor(e.getFunc()), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(tempDots))))) - ), -// LuaAst.LuaLiteral("function(err) " + errorFuncName(tr) + "(tostring(err)) end") - LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end xpcall(function() " + callErrorFunc(tr, "tostring(err)") + " end, function(err2) if err2 == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"error reporting error: \" .. tostring(err2)) BJDebugMsg(\"while reporting: \" .. tostring(err)) end) end") - // unfortunately BJDebugMsg(debug.traceback()) is not working - ) - ), - LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(tempRes)) - ) - ); + LuaStatements callbackBody = LuaAst.LuaStatements(); + if (returnsValue) { + LuaVariable tempRes = LuaAst.LuaVariable("tempRes", LuaAst.LuaExprNull()); + callbackBody.add(tempRes); + callbackBody.add(LuaAst.LuaExprFunctionCallByName("xpcall", + LuaAst.LuaExprlist( + LuaAst.LuaExprFunctionAbstraction( + LuaAst.LuaParams(dots.copy()), + LuaAst.LuaStatements( + LuaAst.LuaAssignment(LuaAst.LuaExprVarAccess(tempRes), + LuaAst.LuaExprFunctionCall(tr.luaFunc.getFor(e.getFunc()), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(dots.copy()))))) + ), + LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"lua callback error: \" .. tostring(err)) xpcall(function() " + callErrorFunc(tr, "tostring(err)") + " end, function(err2) if err2 == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"error reporting error: \" .. tostring(err2)) BJDebugMsg(\"while reporting: \" .. tostring(err)) end) end"), + LuaAst.LuaExprVarAccess(dots.copy()) + ) + )); + callbackBody.add(LuaAst.LuaReturn(LuaAst.LuaExprVarAccess(tempRes))); + } else { + callbackBody.add(LuaAst.LuaExprFunctionCallByName("xpcall", + LuaAst.LuaExprlist( + LuaAst.LuaExprFunctionAbstraction( + LuaAst.LuaParams(dots.copy()), + LuaAst.LuaStatements( + LuaAst.LuaExprFunctionCall(tr.luaFunc.getFor(e.getFunc()), LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(dots.copy()))) + ) + ), + LuaAst.LuaLiteral("function(err) if err == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"lua callback error: \" .. tostring(err)) xpcall(function() " + callErrorFunc(tr, "tostring(err)") + " end, function(err2) if err2 == \"" + WURST_ABORT_THREAD_SENTINEL + "\" then return end BJDebugMsg(\"error reporting error: \" .. tostring(err2)) BJDebugMsg(\"while reporting: \" .. tostring(err)) end) end"), + LuaAst.LuaExprVarAccess(dots.copy()) + ) + )); + } + return LuaAst.LuaExprFunctionAbstraction(LuaAst.LuaParams(dots), callbackBody); } private static String callErrorFunc(LuaTranslator tr, String msg) { @@ -73,6 +101,20 @@ private static String callErrorFunc(LuaTranslator tr, String msg) { } public static LuaExpr translate(ImFunctionCall e, LuaTranslator tr) { + String tcFunc = tr.getTypeCastingFunctionName(e.getFunc()); + if (tcFunc != null && !e.getArguments().isEmpty()) { + LuaExpr arg = e.getArguments().get(0).translateToLua(tr); + if (tcFunc.equals("stringToIndex")) { + return LuaAst.LuaExprFunctionCall(tr.stringToIndexFunction, LuaAst.LuaExprlist(arg)); + } else if (tcFunc.equals("stringFromIndex")) { + return LuaAst.LuaExprFunctionCall(tr.stringFromIndexFunction, LuaAst.LuaExprlist(arg)); + } else if (LUA_HANDLE_TO_INDEX.contains(tcFunc)) { + return LuaAst.LuaExprFunctionCall(tr.toIndexFunction, LuaAst.LuaExprlist(arg)); + } else if (LUA_HANDLE_FROM_INDEX.contains(tcFunc)) { + return LuaAst.LuaExprFunctionCall(tr.fromIndexFunction, LuaAst.LuaExprlist(arg)); + } + } + LuaFunction f = tr.luaFunc.getFor(e.getFunc()); if ("I2S".equals(f.getName()) && isIntentionalThreadAbortCall(e)) { return LuaAst.LuaExprFunctionCallByName("error", LuaAst.LuaExprlist( @@ -341,6 +383,11 @@ public static LuaExpr translate(ImVarAccess e, LuaTranslator tr) { } public static LuaExpr translate(ImVarArrayAccess e, LuaTranslator tr) { + LuaExpr access = translateArrayAccessRaw(e, tr); + return ensureByType(access, e.attrTyp(), tr); + } + + public static LuaExpr translateArrayAccessRaw(ImVarArrayAccess e, LuaTranslator tr) { LuaExprlist indexes = LuaAst.LuaExprlist(); for (ImExpr ie : e.getIndexes()) { indexes.add(ie.translateToLua(tr)); @@ -348,6 +395,22 @@ public static LuaExpr translate(ImVarArrayAccess e, LuaTranslator tr) { return LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(tr.luaVar.getFor(e.getVar())), indexes); } + private static LuaExpr ensureByType(LuaExpr expr, ImType type, LuaTranslator tr) { + if (TypesHelper.isStringType(type)) { + return LuaAst.LuaExprFunctionCall(tr.ensureStrFunction, LuaAst.LuaExprlist(expr)); + } + if (TypesHelper.isIntType(type)) { + return LuaAst.LuaExprFunctionCall(tr.ensureIntFunction, LuaAst.LuaExprlist(expr)); + } + if (TypesHelper.isBoolType(type)) { + return LuaAst.LuaExprFunctionCall(tr.ensureBoolFunction, LuaAst.LuaExprlist(expr)); + } + if (TypesHelper.isRealType(type)) { + return LuaAst.LuaExprFunctionCall(tr.ensureRealFunction, LuaAst.LuaExprlist(expr)); + } + return expr; + } + public static LuaExpr translate(ImGetStackTrace e, LuaTranslator tr) { // return LuaAst.LuaLiteral("debug.traceback()"); return LuaAst.LuaLiteral("\"$Stacktrace$\""); @@ -364,6 +427,9 @@ public static LuaExpr translate(ImTypeVarDispatch imTypeVarDispatch, LuaTranslat public static LuaExpr translate(ImCast imCast, LuaTranslator tr) { LuaExpr translated = imCast.getExpr().translateToLua(tr); if (TypesHelper.isIntType(imCast.getToType())) { + if (TypesHelper.isStringType(imCast.getExpr().attrTyp())) { + return LuaAst.LuaExprFunctionCall(tr.stringToIndexFunction, LuaAst.LuaExprlist(translated)); + } return LuaAst.LuaExprFunctionCall(tr.toIndexFunction, LuaAst.LuaExprlist(translated)); } else if (imCast.getToType() instanceof ImClassType || imCast.getToType() instanceof ImAnyType) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java index de6a17bab..c9c97ab0a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaNatives.java @@ -13,6 +13,28 @@ public class LuaNatives { private static final Map> nativeCodes = new HashMap<>(); + private static final String[] HASHTABLE_HANDLE_SAVE_NAMES = { + "SavePlayerHandle", "SaveWidgetHandle", "SaveDestructableHandle", "SaveItemHandle", "SaveUnitHandle", + "SaveAbilityHandle", "SaveTimerHandle", "SaveTriggerHandle", "SaveTriggerConditionHandle", + "SaveTriggerActionHandle", "SaveTriggerEventHandle", "SaveForceHandle", "SaveGroupHandle", + "SaveLocationHandle", "SaveRectHandle", "SaveBooleanExprHandle", "SaveSoundHandle", "SaveEffectHandle", + "SaveUnitPoolHandle", "SaveItemPoolHandle", "SaveQuestHandle", "SaveQuestItemHandle", + "SaveDefeatConditionHandle", "SaveTimerDialogHandle", "SaveLeaderboardHandle", "SaveMultiboardHandle", + "SaveMultiboardItemHandle", "SaveTrackableHandle", "SaveDialogHandle", "SaveButtonHandle", + "SaveTextTagHandle", "SaveLightningHandle", "SaveImageHandle", "SaveUbersplatHandle", "SaveRegionHandle", + "SaveFogStateHandle", "SaveFogModifierHandle", "SaveAgentHandle", "SaveHashtableHandle", "SaveFrameHandle" + }; + private static final String[] HASHTABLE_HANDLE_LOAD_NAMES = { + "LoadPlayerHandle", "LoadWidgetHandle", "LoadDestructableHandle", "LoadItemHandle", "LoadUnitHandle", + "LoadAbilityHandle", "LoadTimerHandle", "LoadTriggerHandle", "LoadTriggerConditionHandle", + "LoadTriggerActionHandle", "LoadTriggerEventHandle", "LoadForceHandle", "LoadGroupHandle", + "LoadLocationHandle", "LoadRectHandle", "LoadBooleanExprHandle", "LoadSoundHandle", "LoadEffectHandle", + "LoadUnitPoolHandle", "LoadItemPoolHandle", "LoadQuestHandle", "LoadQuestItemHandle", + "LoadDefeatConditionHandle", "LoadTimerDialogHandle", "LoadLeaderboardHandle", "LoadMultiboardHandle", + "LoadMultiboardItemHandle", "LoadTrackableHandle", "LoadDialogHandle", "LoadButtonHandle", + "LoadTextTagHandle", "LoadLightningHandle", "LoadImageHandle", "LoadUbersplatHandle", "LoadRegionHandle", + "LoadFogStateHandle", "LoadFogModifierHandle", "LoadHashtableHandle", "LoadFrameHandle" + }; static { addNative("testSuccess", f -> { @@ -103,54 +125,306 @@ public class LuaNatives { f.getBody().add(LuaAst.LuaLiteral("return math.modf(x)")); }); - addNative(Arrays.asList("InitHashtable", "__wurst_InitHashtable"), f -> f.getBody().add(LuaAst.LuaLiteral("return {}"))); + addNative("__wurst_GetEnumPlayer", f -> { + // Prefer the native enum player when inside an active native ForForce callback. + // This preserves Jass semantics for nested enumerations. + f.getBody().add(LuaAst.LuaLiteral("if GetEnumPlayer ~= nil then")); + f.getBody().add(LuaAst.LuaLiteral(" local p = GetEnumPlayer()")); + f.getBody().add(LuaAst.LuaLiteral(" if p ~= nil then return p end")); + f.getBody().add(LuaAst.LuaLiteral("end")); + f.getBody().add(LuaAst.LuaLiteral("if __wurst_enumPlayer_override ~= nil then return __wurst_enumPlayer_override end")); + f.getBody().add(LuaAst.LuaLiteral("return nil")); + }); + + addNative("__wurst_ForForce", f -> { + f.getParams().add(LuaAst.LuaVariable("whichForce", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("callback", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("if ForForce == nil then return end")); + f.getBody().add(LuaAst.LuaLiteral("local players = {}")); + f.getBody().add(LuaAst.LuaLiteral("local count = 0")); + f.getBody().add(LuaAst.LuaLiteral("local prev = __wurst_enumPlayer_override")); + f.getBody().add(LuaAst.LuaLiteral("ForForce(whichForce, function()")); + f.getBody().add(LuaAst.LuaLiteral(" count = count + 1")); + f.getBody().add(LuaAst.LuaLiteral(" players[count] = __wurst_GetEnumPlayer()")); + f.getBody().add(LuaAst.LuaLiteral("end)")); + f.getBody().add(LuaAst.LuaLiteral("for i = 1, count do")); + f.getBody().add(LuaAst.LuaLiteral(" __wurst_enumPlayer_override = players[i]")); + f.getBody().add(LuaAst.LuaLiteral(" callback()")); + f.getBody().add(LuaAst.LuaLiteral("end")); + f.getBody().add(LuaAst.LuaLiteral("__wurst_enumPlayer_override = prev")); + }); + + addNative("__wurst_GetEnumUnit", f -> { + f.getBody().add(LuaAst.LuaLiteral("if GetEnumUnit ~= nil then")); + f.getBody().add(LuaAst.LuaLiteral(" local u = GetEnumUnit()")); + f.getBody().add(LuaAst.LuaLiteral(" if u ~= nil then return u end")); + f.getBody().add(LuaAst.LuaLiteral("end")); + f.getBody().add(LuaAst.LuaLiteral("if __wurst_enumUnit_override ~= nil then return __wurst_enumUnit_override end")); + f.getBody().add(LuaAst.LuaLiteral("return nil")); + }); + + addNative("__wurst_ForGroup", f -> { + f.getParams().add(LuaAst.LuaVariable("whichGroup", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("callback", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("if ForGroup == nil then return end")); + f.getBody().add(LuaAst.LuaLiteral("local units = {}")); + f.getBody().add(LuaAst.LuaLiteral("local count = 0")); + f.getBody().add(LuaAst.LuaLiteral("local prev = __wurst_enumUnit_override")); + f.getBody().add(LuaAst.LuaLiteral("ForGroup(whichGroup, function()")); + f.getBody().add(LuaAst.LuaLiteral(" count = count + 1")); + f.getBody().add(LuaAst.LuaLiteral(" units[count] = __wurst_GetEnumUnit()")); + f.getBody().add(LuaAst.LuaLiteral("end)")); + f.getBody().add(LuaAst.LuaLiteral("for i = 1, count do")); + f.getBody().add(LuaAst.LuaLiteral(" __wurst_enumUnit_override = units[i]")); + f.getBody().add(LuaAst.LuaLiteral(" callback()")); + f.getBody().add(LuaAst.LuaLiteral("end")); + f.getBody().add(LuaAst.LuaLiteral("__wurst_enumUnit_override = prev")); + }); + + addNative("__wurst_GetEnumItem", f -> { + f.getBody().add(LuaAst.LuaLiteral("if GetEnumItem ~= nil then")); + f.getBody().add(LuaAst.LuaLiteral(" local it = GetEnumItem()")); + f.getBody().add(LuaAst.LuaLiteral(" if it ~= nil then return it end")); + f.getBody().add(LuaAst.LuaLiteral("end")); + f.getBody().add(LuaAst.LuaLiteral("if __wurst_enumItem_override ~= nil then return __wurst_enumItem_override end")); + f.getBody().add(LuaAst.LuaLiteral("return nil")); + }); - addNative(Arrays.asList( - "SaveInteger", "SaveBoolean", "SaveReal", "SaveStr", - "__wurst_SaveInteger", "__wurst_SaveBoolean", "__wurst_SaveReal", "__wurst_SaveStr"), f -> { + addNative("__wurst_EnumItemsInRect", f -> { + f.getParams().add(LuaAst.LuaVariable("r", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("filter", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("actionFunc", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("if EnumItemsInRect == nil then return end")); + f.getBody().add(LuaAst.LuaLiteral("local items = {}")); + f.getBody().add(LuaAst.LuaLiteral("local count = 0")); + f.getBody().add(LuaAst.LuaLiteral("local prev = __wurst_enumItem_override")); + f.getBody().add(LuaAst.LuaLiteral("EnumItemsInRect(r, filter, function()")); + f.getBody().add(LuaAst.LuaLiteral(" count = count + 1")); + f.getBody().add(LuaAst.LuaLiteral(" items[count] = __wurst_GetEnumItem()")); + f.getBody().add(LuaAst.LuaLiteral("end)")); + f.getBody().add(LuaAst.LuaLiteral("for i = 1, count do")); + f.getBody().add(LuaAst.LuaLiteral(" __wurst_enumItem_override = items[i]")); + f.getBody().add(LuaAst.LuaLiteral(" actionFunc()")); + f.getBody().add(LuaAst.LuaLiteral("end")); + f.getBody().add(LuaAst.LuaLiteral("__wurst_enumItem_override = prev")); + }); + + addNative("__wurst_GetEnumDestructable", f -> { + f.getBody().add(LuaAst.LuaLiteral("if GetEnumDestructable ~= nil then")); + f.getBody().add(LuaAst.LuaLiteral(" local d = GetEnumDestructable()")); + f.getBody().add(LuaAst.LuaLiteral(" if d ~= nil then return d end")); + f.getBody().add(LuaAst.LuaLiteral("end")); + f.getBody().add(LuaAst.LuaLiteral("if __wurst_enumDestructable_override ~= nil then return __wurst_enumDestructable_override end")); + f.getBody().add(LuaAst.LuaLiteral("return nil")); + }); + + addNative("__wurst_EnumDestructablesInRect", f -> { + f.getParams().add(LuaAst.LuaVariable("r", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("filter", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("actionFunc", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("if EnumDestructablesInRect == nil then return end")); + f.getBody().add(LuaAst.LuaLiteral("local dests = {}")); + f.getBody().add(LuaAst.LuaLiteral("local count = 0")); + f.getBody().add(LuaAst.LuaLiteral("local prev = __wurst_enumDestructable_override")); + f.getBody().add(LuaAst.LuaLiteral("EnumDestructablesInRect(r, filter, function()")); + f.getBody().add(LuaAst.LuaLiteral(" count = count + 1")); + f.getBody().add(LuaAst.LuaLiteral(" dests[count] = __wurst_GetEnumDestructable()")); + f.getBody().add(LuaAst.LuaLiteral("end)")); + f.getBody().add(LuaAst.LuaLiteral("for i = 1, count do")); + f.getBody().add(LuaAst.LuaLiteral(" __wurst_enumDestructable_override = dests[i]")); + f.getBody().add(LuaAst.LuaLiteral(" actionFunc()")); + f.getBody().add(LuaAst.LuaLiteral("end")); + f.getBody().add(LuaAst.LuaLiteral("__wurst_enumDestructable_override = prev")); + }); + + addNative(Arrays.asList("InitHashtable", "__wurst_InitHashtable"), f -> + f.getBody().add(LuaAst.LuaLiteral("return { __wurst_ht_int = {}, __wurst_ht_bool = {}, __wurst_ht_real = {}, __wurst_ht_str = {}, __wurst_ht_handle = {} }"))); + + addNative(Arrays.asList("SaveInteger", "__wurst_SaveInteger"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_int")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_int = t end")); + f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); + }); + addNative(Arrays.asList("SaveBoolean", "__wurst_SaveBoolean"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_bool")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_bool = t end")); + f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); + }); + addNative(Arrays.asList("SaveReal", "__wurst_SaveReal"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_real")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_real = t end")); + f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); + }); + addNative(Arrays.asList("SaveStr", "__wurst_SaveStr"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("if not h[p] then h[p] = {} end h[p][c] = i")); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_str")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_str = t end")); + f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); + }); + addNative(withWurstPrefix(HASHTABLE_HANDLE_SAVE_NAMES), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("i", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_handle")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil then t = {}; h.__wurst_ht_handle = t end")); + f.getBody().add(LuaAst.LuaLiteral("if not t[p] then t[p] = {} end t[p][c] = i")); + }); + + addNative(Arrays.asList("LoadInteger", "__wurst_LoadInteger"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_int")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return 0 end")); + f.getBody().add(LuaAst.LuaLiteral("local v = t[p][c]")); + f.getBody().add(LuaAst.LuaLiteral("if v == nil then return 0 end")); + f.getBody().add(LuaAst.LuaLiteral("return v")); }); - addNative(Arrays.asList( - "LoadInteger", "LoadBoolean", "LoadReal", "LoadStr", - "__wurst_LoadInteger", "__wurst_LoadBoolean", "__wurst_LoadReal", "__wurst_LoadStr"), f -> { + addNative(Arrays.asList("LoadBoolean", "__wurst_LoadBoolean"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("if not h[p] then return nil end return h[p][c]")); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_bool")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return false end")); + f.getBody().add(LuaAst.LuaLiteral("local v = t[p][c]")); + f.getBody().add(LuaAst.LuaLiteral("if v == nil then return false end")); + f.getBody().add(LuaAst.LuaLiteral("return v")); }); - addNative(Arrays.asList( - "HaveSavedInteger", "HaveSavedBoolean", "HaveSavedReal", "HaveSavedString", - "__wurst_HaveSavedInteger", "__wurst_HaveSavedBoolean", "__wurst_HaveSavedReal", "__wurst_HaveSavedString"), f -> { + addNative(Arrays.asList("LoadReal", "__wurst_LoadReal"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("return h[p] ~= nil and h[p][c] ~= nil")); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_real")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return 0.0 end")); + f.getBody().add(LuaAst.LuaLiteral("local v = t[p][c]")); + f.getBody().add(LuaAst.LuaLiteral("if v == nil then return 0.0 end")); + f.getBody().add(LuaAst.LuaLiteral("return v")); + }); + + addNative(Arrays.asList("LoadStr", "__wurst_LoadStr"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_str")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return nil end")); + f.getBody().add(LuaAst.LuaLiteral("return t[p][c]")); + }); + addNative(withWurstPrefix(HASHTABLE_HANDLE_LOAD_NAMES), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_handle")); + f.getBody().add(LuaAst.LuaLiteral("if t == nil or t[p] == nil then return nil end")); + f.getBody().add(LuaAst.LuaLiteral("return t[p][c]")); + }); + + addNative(Arrays.asList("HaveSavedInteger", "__wurst_HaveSavedInteger"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_int")); + f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); + }); + addNative(Arrays.asList("HaveSavedBoolean", "__wurst_HaveSavedBoolean"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_bool")); + f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); + }); + addNative(Arrays.asList("HaveSavedReal", "__wurst_HaveSavedReal"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_real")); + f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); + }); + addNative(Arrays.asList("HaveSavedString", "__wurst_HaveSavedString"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_str")); + f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); + }); + addNative(Arrays.asList("HaveSavedHandle", "__wurst_HaveSavedHandle"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_handle")); + f.getBody().add(LuaAst.LuaLiteral("return t ~= nil and t[p] ~= nil and t[p][c] ~= nil")); }); addNative(Arrays.asList("FlushChildHashtable", "__wurst_FlushChildHashtable"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("h[p] = nil")); + f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_int then h.__wurst_ht_int[p] = nil end")); + f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_bool then h.__wurst_ht_bool[p] = nil end")); + f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_real then h.__wurst_ht_real[p] = nil end")); + f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_str then h.__wurst_ht_str[p] = nil end")); + f.getBody().add(LuaAst.LuaLiteral("if h.__wurst_ht_handle then h.__wurst_ht_handle[p] = nil end")); }); addNative(Arrays.asList("FlushParentHashtable", "__wurst_FlushParentHashtable"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("for k in pairs(h) do h[k] = nil end")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_int = {}")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_bool = {}")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_real = {}")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_str = {}")); + f.getBody().add(LuaAst.LuaLiteral("h.__wurst_ht_handle = {}")); }); - addNative(Arrays.asList( - "RemoveSavedInteger", "RemoveSavedBoolean", "RemoveSavedReal", "RemoveSavedString", - "__wurst_RemoveSavedInteger", "__wurst_RemoveSavedBoolean", "__wurst_RemoveSavedReal", "__wurst_RemoveSavedString"), f -> { + addNative(Arrays.asList("RemoveSavedInteger", "__wurst_RemoveSavedInteger"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_int")); + f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); + }); + addNative(Arrays.asList("RemoveSavedBoolean", "__wurst_RemoveSavedBoolean"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_bool")); + f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); + }); + addNative(Arrays.asList("RemoveSavedReal", "__wurst_RemoveSavedReal"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_real")); + f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); + }); + addNative(Arrays.asList("RemoveSavedString", "__wurst_RemoveSavedString"), f -> { f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); - f.getBody().add(LuaAst.LuaLiteral("if h[p] then h[p][c] = nil end")); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_str")); + f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); + }); + addNative(Arrays.asList("RemoveSavedHandle", "__wurst_RemoveSavedHandle"), f -> { + f.getParams().add(LuaAst.LuaVariable("h", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("p", LuaAst.LuaNoExpr())); + f.getParams().add(LuaAst.LuaVariable("c", LuaAst.LuaNoExpr())); + f.getBody().add(LuaAst.LuaLiteral("local t = h.__wurst_ht_handle")); + f.getBody().add(LuaAst.LuaLiteral("if t ~= nil and t[p] then t[p][c] = nil end")); }); addNative("typeIdToTypeName", f -> { @@ -187,6 +461,15 @@ private static void addNative(Iterable names, Consumer g) { } } + private static Iterable withWurstPrefix(String[] names) { + java.util.List result = new java.util.ArrayList<>(); + for (String name : names) { + result.add(name); + result.add("__wurst_" + name); + } + return result; + } + public static void get(LuaFunction f) { nativeCodes.getOrDefault(f.getName(), ff -> { // generate a runtime exception diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index 78e27590a..51cbdde46 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -16,19 +16,78 @@ import org.jetbrains.annotations.NotNull; import java.util.*; +import java.util.regex.Pattern; import java.util.stream.Stream; import static de.peeeq.wurstscript.translation.lua.translation.ExprTranslation.WURST_SUPERTYPES; public class LuaTranslator { - private static final Set HASHTABLE_NATIVE_NAMES = new HashSet<>(Arrays.asList( + private static final int LUA_LOCALS_LIMIT = 200; + private static final List HASHTABLE_HANDLE_SAVE_NAMES = Arrays.asList( + "SavePlayerHandle", "SaveWidgetHandle", "SaveDestructableHandle", "SaveItemHandle", "SaveUnitHandle", + "SaveAbilityHandle", "SaveTimerHandle", "SaveTriggerHandle", "SaveTriggerConditionHandle", + "SaveTriggerActionHandle", "SaveTriggerEventHandle", "SaveForceHandle", "SaveGroupHandle", + "SaveLocationHandle", "SaveRectHandle", "SaveBooleanExprHandle", "SaveSoundHandle", "SaveEffectHandle", + "SaveUnitPoolHandle", "SaveItemPoolHandle", "SaveQuestHandle", "SaveQuestItemHandle", + "SaveDefeatConditionHandle", "SaveTimerDialogHandle", "SaveLeaderboardHandle", "SaveMultiboardHandle", + "SaveMultiboardItemHandle", "SaveTrackableHandle", "SaveDialogHandle", "SaveButtonHandle", + "SaveTextTagHandle", "SaveLightningHandle", "SaveImageHandle", "SaveUbersplatHandle", "SaveRegionHandle", + "SaveFogStateHandle", "SaveFogModifierHandle", "SaveAgentHandle", "SaveHashtableHandle", "SaveFrameHandle" + ); + private static final List HASHTABLE_HANDLE_LOAD_NAMES = Arrays.asList( + "LoadPlayerHandle", "LoadWidgetHandle", "LoadDestructableHandle", "LoadItemHandle", "LoadUnitHandle", + "LoadAbilityHandle", "LoadTimerHandle", "LoadTriggerHandle", "LoadTriggerConditionHandle", + "LoadTriggerActionHandle", "LoadTriggerEventHandle", "LoadForceHandle", "LoadGroupHandle", + "LoadLocationHandle", "LoadRectHandle", "LoadBooleanExprHandle", "LoadSoundHandle", "LoadEffectHandle", + "LoadUnitPoolHandle", "LoadItemPoolHandle", "LoadQuestHandle", "LoadQuestItemHandle", + "LoadDefeatConditionHandle", "LoadTimerDialogHandle", "LoadLeaderboardHandle", "LoadMultiboardHandle", + "LoadMultiboardItemHandle", "LoadTrackableHandle", "LoadDialogHandle", "LoadButtonHandle", + "LoadTextTagHandle", "LoadLightningHandle", "LoadImageHandle", "LoadUbersplatHandle", "LoadRegionHandle", + "LoadFogStateHandle", "LoadFogModifierHandle", "LoadHashtableHandle", "LoadFrameHandle" + ); + private static final List HASHTABLE_NATIVE_NAMES_RAW = Arrays.asList( "InitHashtable", "SaveInteger", "SaveBoolean", "SaveReal", "SaveStr", "LoadInteger", "LoadBoolean", "LoadReal", "LoadStr", - "HaveSavedInteger", "HaveSavedBoolean", "HaveSavedReal", "HaveSavedString", + "HaveSavedInteger", "HaveSavedBoolean", "HaveSavedReal", "HaveSavedString", "HaveSavedHandle", "FlushChildHashtable", "FlushParentHashtable", - "RemoveSavedInteger", "RemoveSavedBoolean", "RemoveSavedReal", "RemoveSavedString" - )); + "RemoveSavedInteger", "RemoveSavedBoolean", "RemoveSavedReal", "RemoveSavedString", "RemoveSavedHandle" + ); + private static final List REQUIRED_WURST_HASHTABLE_HELPERS = Arrays.asList( + "__wurst_InitHashtable", + "__wurst_SaveInteger", "__wurst_SaveBoolean", "__wurst_SaveReal", "__wurst_SaveStr", + "__wurst_LoadInteger", "__wurst_LoadBoolean", "__wurst_LoadReal", "__wurst_LoadStr", + "__wurst_HaveSavedInteger", "__wurst_HaveSavedBoolean", "__wurst_HaveSavedReal", "__wurst_HaveSavedString", "__wurst_HaveSavedHandle", + "__wurst_FlushChildHashtable", "__wurst_FlushParentHashtable", + "__wurst_RemoveSavedInteger", "__wurst_RemoveSavedBoolean", "__wurst_RemoveSavedReal", "__wurst_RemoveSavedString", "__wurst_RemoveSavedHandle" + ); + private static final List REQUIRED_WURST_CONTEXT_CALLBACK_HELPERS = Arrays.asList( + "__wurst_ForForce", + "__wurst_GetEnumPlayer", + "__wurst_ForGroup", + "__wurst_GetEnumUnit", + "__wurst_EnumItemsInRect", + "__wurst_GetEnumItem", + "__wurst_EnumDestructablesInRect", + "__wurst_GetEnumDestructable" + ); + private static final Set HASHTABLE_NATIVE_NAMES = new HashSet<>(allHashtableNativeNames()); + private static final Set LUA_HANDLE_TO_INDEX = Set.of( + "widgetToIndex", "unitToIndex", "destructableToIndex", "itemToIndex", "abilityToIndex", + "forceToIndex", "groupToIndex", "triggerToIndex", "triggeractionToIndex", "triggerconditionToIndex", + "timerToIndex", "locationToIndex", "regionToIndex", "rectToIndex", "soundToIndex", + "effectToIndex", "dialogToIndex", "buttonToIndex", "questToIndex", "questitemToIndex", + "leaderboardToIndex", "multiboardToIndex", "trackableToIndex", "lightningToIndex", + "ubersplatToIndex", "framehandleToIndex", "oskeytypeToIndex" + ); + private static final Set LUA_HANDLE_FROM_INDEX = Set.of( + "widgetFromIndex", "unitFromIndex", "destructableFromIndex", "itemFromIndex", "abilityFromIndex", + "forceFromIndex", "groupFromIndex", "triggerFromIndex", "triggeractionFromIndex", "triggerconditionFromIndex", + "timerFromIndex", "locationFromIndex", "regionFromIndex", "rectFromIndex", "soundFromIndex", + "effectFromIndex", "dialogFromIndex", "buttonFromIndex", "questFromIndex", "questitemFromIndex", + "leaderboardFromIndex", "multiboardFromIndex", "trackableFromIndex", "lightningFromIndex", + "ubersplatFromIndex", "framehandleFromIndex", "oskeytypeFromIndex" + ); final ImProg prog; final LuaCompilationUnit luaModel; @@ -137,6 +196,8 @@ public LuaMethod initFor(ImClass a) { LuaFunction toIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_objectToIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); LuaFunction fromIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_objectFromIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); + LuaFunction stringToIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_stringToIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); + LuaFunction stringFromIndexFunction = LuaAst.LuaFunction(uniqueName("__wurst_stringFromIndex"), LuaAst.LuaParams(), LuaAst.LuaStatements()); LuaFunction instanceOfFunction = LuaAst.LuaFunction(uniqueName("isInstanceOf"), LuaAst.LuaParams(), LuaAst.LuaStatements()); @@ -162,7 +223,6 @@ public LuaMethod initFor(ImClass a) { return Stream.empty(); }) .findFirst().orElse(null))); - private final ImTranslator imTr; public LuaTranslator(ImProg prog, ImTranslator imTr) { @@ -172,6 +232,30 @@ public LuaTranslator(ImProg prog, ImTranslator imTr) { } private String remapNativeName(String name) { + if ("ForForce".equals(name)) { + return "__wurst_ForForce"; + } + if ("GetEnumPlayer".equals(name)) { + return "__wurst_GetEnumPlayer"; + } + if ("ForGroup".equals(name)) { + return "__wurst_ForGroup"; + } + if ("GetEnumUnit".equals(name)) { + return "__wurst_GetEnumUnit"; + } + if ("EnumItemsInRect".equals(name)) { + return "__wurst_EnumItemsInRect"; + } + if ("GetEnumItem".equals(name)) { + return "__wurst_GetEnumItem"; + } + if ("EnumDestructablesInRect".equals(name)) { + return "__wurst_EnumDestructablesInRect"; + } + if ("GetEnumDestructable".equals(name)) { + return "__wurst_GetEnumDestructable"; + } if (HASHTABLE_NATIVE_NAMES.contains(name)) { return "__wurst_" + name; } @@ -200,7 +284,10 @@ public LuaCompilationUnit translate() { createStringConcatFunction(); createInstanceOfFunction(); createObjectIndexFunctions(); + createStringIndexFunctions(); createEnsureTypeFunctions(); + ensureWurstHashtableHelpers(); + ensureWurstContextCallbackHelpers(); for (ImVar v : prog.getGlobals()) { translateGlobal(v); @@ -225,10 +312,87 @@ public LuaCompilationUnit translate() { } cleanStatements(); + emitExperimentalHashtableLeakGuards(); return luaModel; } + /** + * Always emit internal hashtable helper functions used by Lua lowering. + * This keeps compiletime migration data loading robust even if the + * corresponding Warcraft natives are unavailable or filtered out. + */ + private void ensureWurstHashtableHelpers() { + Set requiredHelpers = new LinkedHashSet<>(REQUIRED_WURST_HASHTABLE_HELPERS); + requiredHelpers.addAll(prefixed(HASHTABLE_HANDLE_SAVE_NAMES)); + requiredHelpers.addAll(prefixed(HASHTABLE_HANDLE_LOAD_NAMES)); + for (String helper : requiredHelpers) { + LuaFunction f = LuaAst.LuaFunction(helper, LuaAst.LuaParams(), LuaAst.LuaStatements()); + LuaNatives.get(f); + luaModel.add(f); + } + } + + private void ensureWurstContextCallbackHelpers() { + for (String helper : REQUIRED_WURST_CONTEXT_CALLBACK_HELPERS) { + LuaFunction f = LuaAst.LuaFunction(helper, LuaAst.LuaParams(), LuaAst.LuaStatements()); + LuaNatives.get(f); + luaModel.add(f); + } + } + + private void emitExperimentalHashtableLeakGuards() { + luaModel.add(LuaAst.LuaLiteral("-- Wurst experimental Lua assertion guards: raw WC3 hashtable natives must not be called.")); + for (String nativeName : allHashtableNativeNames()) { + luaModel.add(LuaAst.LuaLiteral("if " + nativeName + " ~= nil then " + nativeName + + " = function(...) error(\"Wurst Lua assertion failed: unexpected call to native " + nativeName + + ". Expected __wurst_" + nativeName + ".\") end end")); + } + } + + public static void assertNoLeakedHashtableNativeCalls(String luaCode) { + List leaked = new ArrayList<>(); + List missingHelpers = new ArrayList<>(); + for (String nativeName : allHashtableNativeNames()) { + if (containsRegex(luaCode, "\\b" + nativeName + "\\s*\\(")) { + leaked.add(nativeName); + } + String helperName = "__wurst_" + nativeName; + boolean helperCalled = containsRegex(luaCode, "\\b" + helperName + "\\s*\\("); + boolean helperDefined = containsRegex(luaCode, "\\bfunction\\s+" + helperName + "\\s*\\("); + if (helperCalled && !helperDefined) { + missingHelpers.add(helperName); + } + } + if (!leaked.isEmpty()) { + throw new RuntimeException("Wurst Lua backend assertion failed: leaked raw hashtable native calls in generated Lua: " + + String.join(", ", leaked)); + } + if (!missingHelpers.isEmpty()) { + throw new RuntimeException("Wurst Lua backend assertion failed: missing __wurst hashtable helper definitions in generated Lua: " + + String.join(", ", missingHelpers)); + } + } + + private static List allHashtableNativeNames() { + List result = new ArrayList<>(HASHTABLE_NATIVE_NAMES_RAW); + result.addAll(HASHTABLE_HANDLE_SAVE_NAMES); + result.addAll(HASHTABLE_HANDLE_LOAD_NAMES); + return result; + } + + private static List prefixed(List names) { + List result = new ArrayList<>(); + for (String name : names) { + result.add("__wurst_" + name); + } + return result; + } + + private static boolean containsRegex(String text, String regex) { + return Pattern.compile(regex).matcher(text).find(); + } + private boolean isFixedEntryPoint(ImFunction function) { return function == imTr.getMainFunc() || function == imTr.getConfFunc(); } @@ -411,6 +575,65 @@ private void createObjectIndexFunctions() { } } + private void createStringIndexFunctions() { + LuaVariable map = LuaAst.LuaVariable("__wurst_string_index_map", LuaAst.LuaTableConstructor(LuaAst.LuaTableFields( + LuaAst.LuaTableNamedField("counter", LuaAst.LuaExprIntVal("0")), + LuaAst.LuaTableNamedField("byString", LuaAst.LuaTableConstructor(LuaAst.LuaTableFields())), + LuaAst.LuaTableNamedField("byIndex", LuaAst.LuaTableConstructor(LuaAst.LuaTableFields())) + ))); + luaModel.add(map); + + { + String[] code = { + "if x == nil then", + " return 0", + "end", + "if type(x) ~= \"string\" then", + " x = tostring(x)", + "end", + "local id = __wurst_string_index_map.byString[x]", + "if id ~= nil then", + " return id", + "end", + "id = __wurst_string_index_map.counter + 1", + "__wurst_string_index_map.counter = id", + "__wurst_string_index_map.byString[x] = id", + "__wurst_string_index_map.byIndex[id] = x", + "return id" + }; + + stringToIndexFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); + for (String c : code) { + stringToIndexFunction.getBody().add(LuaAst.LuaLiteral(c)); + } + luaModel.add(stringToIndexFunction); + } + + { + String[] code = { + "local id = tonumber(x)", + "if id == nil then", + " return \"\"", + "end", + "id = math.tointeger(id)", + "if id == nil then", + " return \"\"", + "end", + "local s = __wurst_string_index_map.byIndex[id]", + "if s == nil then", + " return \"\"", + "end", + "return s" + }; + + stringFromIndexFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); + for (String c : code) { + stringFromIndexFunction.getBody().add(LuaAst.LuaLiteral(c)); + } + luaModel.add(stringFromIndexFunction); + } + } + private void createArrayInitFunction() { /* function defaultArray(d) @@ -443,24 +666,29 @@ function defaultArray(d) } private void createEnsureTypeFunctions() { - LuaFunction[] ensureTypeFunctions = {ensureIntFunction, ensureBoolFunction, ensureRealFunction, ensureStrFunction}; - String[] defaultValue = {"0", "false", "0.0", "\"\""}; - for(int i = 0; i < ensureTypeFunctions.length; ++i) { - LuaFunction ensureTypeFunction = ensureTypeFunctions[i]; - String[] code = { - "if x == nil then", - " return " + defaultValue[i], - "else", - " return " + (ensureTypeFunction == ensureIntFunction ? "math.tointeger(x)" : "x"), - "end" - }; - - ensureTypeFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); - for (String c : code) { - ensureTypeFunction.getBody().add(LuaAst.LuaLiteral(c)); - } - luaModel.add(ensureTypeFunction); - } + ensureIntFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); + ensureIntFunction.getBody().add(LuaAst.LuaLiteral("local n = tonumber(x)")); + ensureIntFunction.getBody().add(LuaAst.LuaLiteral("if n == nil then return 0 end")); + ensureIntFunction.getBody().add(LuaAst.LuaLiteral("local i = math.tointeger(n)")); + ensureIntFunction.getBody().add(LuaAst.LuaLiteral("if i == nil then return 0 end")); + ensureIntFunction.getBody().add(LuaAst.LuaLiteral("return i")); + luaModel.add(ensureIntFunction); + + ensureBoolFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); + ensureBoolFunction.getBody().add(LuaAst.LuaLiteral("if x == nil then return false end")); + ensureBoolFunction.getBody().add(LuaAst.LuaLiteral("return x")); + luaModel.add(ensureBoolFunction); + + ensureRealFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); + ensureRealFunction.getBody().add(LuaAst.LuaLiteral("local n = tonumber(x)")); + ensureRealFunction.getBody().add(LuaAst.LuaLiteral("if n == nil then return 0.0 end")); + ensureRealFunction.getBody().add(LuaAst.LuaLiteral("return n")); + luaModel.add(ensureRealFunction); + + ensureStrFunction.getParams().add(LuaAst.LuaVariable("x", LuaAst.LuaNoExpr())); + ensureStrFunction.getBody().add(LuaAst.LuaLiteral("if x == nil then return \"\" end")); + ensureStrFunction.getBody().add(LuaAst.LuaLiteral("return tostring(x)")); + luaModel.add(ensureStrFunction); } private void cleanStatements() { @@ -500,6 +728,10 @@ private void translateFunc(ImFunction f) { if (f.isNative()) { LuaNatives.get(lf); } else { + if (rewriteTypeCastingCompatFunction(f, lf)) { + luaModel.add(lf); + return; + } if (f.hasFlag(FunctionFlagEnum.IS_VARARG)) { @@ -508,14 +740,17 @@ private void translateFunc(ImFunction f) { } // translate local variables + List functionLocals = new ArrayList<>(); for (ImVar local : f.getLocals()) { LuaVariable luaLocal = luaVar.getFor(local); luaLocal.setInitialValue(defaultValue(local.getType())); lf.getBody().add(luaLocal); + functionLocals.add(luaLocal); } // translate body: translateStatements(lf.getBody(), f.getBody()); + spillLocalsIntoTableIfNeeded(lf, functionLocals); } if (f.isExtern() || f.isNative()) { @@ -536,6 +771,91 @@ private void translateFunc(ImFunction f) { } } + private boolean rewriteTypeCastingCompatFunction(ImFunction f, LuaFunction lf) { + if (f.getParameters().isEmpty()) { + return false; + } + String tcFunc = f.getName(); + ImVar p = f.getParameters().get(0); + LuaExpr arg = LuaAst.LuaExprVarAccess(luaVar.getFor(p)); + + if ("stringToIndex".equals(tcFunc)) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCall(stringToIndexFunction, LuaAst.LuaExprlist(arg)))); + return true; + } + if ("stringFromIndex".equals(tcFunc)) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCall(stringFromIndexFunction, LuaAst.LuaExprlist(arg)))); + return true; + } + // Keep semantic conversions for primitive/index-domain helpers intact. + if ("realToIndex".equals(tcFunc) || "realFromIndex".equals(tcFunc) + || "playerToIndex".equals(tcFunc) || "playerFromIndex".equals(tcFunc) + || "booleanToIndex".equals(tcFunc) || "booleanFromIndex".equals(tcFunc)) { + return false; + } + if (LUA_HANDLE_TO_INDEX.contains(tcFunc)) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCall(toIndexFunction, LuaAst.LuaExprlist(arg)))); + return true; + } + if (LUA_HANDLE_FROM_INDEX.contains(tcFunc)) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCall(fromIndexFunction, LuaAst.LuaExprlist(arg)))); + return true; + } + // Final fallback for transformed/copied function names that may lose trace info: + if (tcFunc.endsWith("ToIndex")) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCall(toIndexFunction, LuaAst.LuaExprlist(arg)))); + return true; + } + if (tcFunc.endsWith("FromIndex")) { + lf.getBody().clear(); + lf.getBody().add(LuaAst.LuaReturn(LuaAst.LuaExprFunctionCall(fromIndexFunction, LuaAst.LuaExprlist(arg)))); + return true; + } + return false; + } + + private void spillLocalsIntoTableIfNeeded(LuaFunction lf, List functionLocals) { + if (functionLocals.size() <= LUA_LOCALS_LIMIT) { + return; + } + + Set localSet = new HashSet<>(functionLocals); + LuaVariable localsTable = LuaAst.LuaVariable(uniqueName("__wurst_locals"), + LuaAst.LuaTableConstructor(LuaAst.LuaTableFields())); + + // Rewrite accesses first, then replace declarations with table init assignments. + lf.getBody().forEachElement(e -> { + if (e instanceof LuaExprVarAccess) { + LuaExprVarAccess va = (LuaExprVarAccess) e; + if (localSet.contains(va.getVar())) { + LuaExpr tableRef = LuaAst.LuaExprVarAccess(localsTable); + LuaExpr key = LuaAst.LuaExprStringVal(va.getVar().getName()); + va.replaceBy(LuaAst.LuaExprArrayAccess(tableRef, LuaAst.LuaExprlist(key))); + } + } + }); + + List oldBody = new ArrayList<>(lf.getBody()); + lf.getBody().clear(); + lf.getBody().add(localsTable); + + for (LuaStatement stmt : oldBody) { + if (stmt instanceof LuaVariable && localSet.contains(stmt)) { + LuaVariable localDecl = (LuaVariable) stmt; + LuaExpr key = LuaAst.LuaExprStringVal(localDecl.getName()); + LuaExpr left = LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(localsTable), LuaAst.LuaExprlist(key)); + lf.getBody().add(LuaAst.LuaAssignment(left, ((LuaExpr) localDecl.getInitialValue()).copy())); + } else { + lf.getBody().add(stmt); + } + } + } + void translateStatements(List res, ImStmts stmts) { for (ImStmt s : stmts) { s.translateStmtToLua(res, this); @@ -721,6 +1041,8 @@ public LuaExpr case_ImSimpleType(ImSimpleType st) { return LuaAst.LuaExprBoolVal(false); } else if (TypesHelper.isRealType(st)) { return LuaAst.LuaExprRealVal("0."); + } else if (TypesHelper.isStringType(st)) { + return LuaAst.LuaExprStringVal(""); } return LuaAst.LuaExprNull(); } @@ -770,4 +1092,21 @@ public int getTypeId(ImClass classDef) { public LuaFunction getErrorFunc() { return errorFunc.get(); } + + public String getTypeCastingFunctionName(ImFunction f) { + Element trace = f.attrTrace(); + if (trace instanceof FuncDef fd && fd.attrNearestPackage() instanceof WPackage p) { + if ("TypeCasting".equals(p.getName())) { + return fd.getName(); + } + } + // Fallback: transformed/copied IM functions may lose package trace metadata. + // Keep Lua behavior stable by routing canonical *ToIndex/*FromIndex names anyway. + String name = f.getName(); + if ("stringToIndex".equals(name) || "stringFromIndex".equals(name) + || name.endsWith("ToIndex") || name.endsWith("FromIndex")) { + return name; + } + return null; + } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java index 895146198..93e7e7bab 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/StmtTranslation.java @@ -34,7 +34,13 @@ public static void translate(ImReturn s, List res, LuaTranslator t } public static void translate(ImSet s, List res, LuaTranslator tr) { - LuaExpr left = s.getLeft().translateToLua(tr); + LuaExpr left; + if (s.getLeft() instanceof ImVarArrayAccess) { + // Assignment LHS must stay a writable table access, never an ensured r-value wrapper. + left = ExprTranslation.translateArrayAccessRaw((ImVarArrayAccess) s.getLeft(), tr); + } else { + left = s.getLeft().translateToLua(tr); + } LuaExpr right = s.getRight().translateToLua(tr); if (s.getRight().attrTyp() instanceof ImTupleType) { ImTupleType tt = (ImTupleType) s.getRight().attrTyp(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 7facfae8b..b947798f4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -493,6 +493,8 @@ private void check(Element e) { } if (e instanceof StmtExitwhen) visit((StmtExitwhen) e); + if (e instanceof StmtContinue) + visit((StmtContinue) e); } catch (CyclicDependencyError cde) { cde.printStackTrace(); Element element = cde.getElement(); @@ -616,6 +618,23 @@ private void visit(StmtExitwhen exitwhen) { exitwhen.addError("Break is not allowed outside of loop statements."); } + private void visit(StmtContinue stmtContinue) { + Element parent = stmtContinue.getParent(); + while (!(parent instanceof FunctionDefinition)) { + if (parent instanceof StmtForEach) { + StmtForEach forEach = (StmtForEach) parent; + if (forEach.getIn().tryGetNameDef().attrIsVararg()) { + stmtContinue.addError("Cannot use continue in vararg for each loops."); + } + return; + } else if (parent instanceof LoopStatement) { + return; + } + parent = parent.getParent(); + } + stmtContinue.addError("Continue is not allowed outside of loop statements."); + } + private void checkTupleDef(TupleDef e) { checkTupleDefCycle(e, new ArrayList<>()); @@ -1012,10 +1031,11 @@ private void checkClosure(ExprClosure e) { if (expectedTyp instanceof WurstTypeCode) { // TODO check if no vars are captured if (!e.attrCapturedVariables().isEmpty()) { + String codeLambdaContext = codeLambdaContext(e); for (Map.Entry elem : e.attrCapturedVariables().entries()) { elem.getKey().addError("Cannot capture local variable '" + elem.getValue().getName() - + "' in anonymous function. This is only possible with closures."); + + "' in anonymous function" + codeLambdaContext + ". This is only possible with closures."); } } } else if (expectedTyp instanceof WurstTypeUnknown || expectedTyp instanceof WurstTypeClosure) { @@ -1062,6 +1082,21 @@ private void checkClosure(ExprClosure e) { } + private static String codeLambdaContext(ExprClosure e) { + Element parent = e.getParent(); + if (parent instanceof Arguments args) { + Element call = args.getParent(); + if (call instanceof StmtCall stmtCall) { + String funcName = stmtCall instanceof FunctionCall fc ? fc.getFuncName() : ""; + if (stmtCall instanceof ExprMemberMethod) { + return " passed as code to ." + funcName + "() ->"; + } + return " passed as code to " + funcName + "() ->"; + } + } + return ""; + } + private void checkConstructorsUnique(ClassOrModule c) { List constrs = c.getConstructors(); @@ -1841,7 +1876,7 @@ private void visit(Modifiers modifiers) { private void visit(StmtReturn s) { if (s.attrNearestExprStatementsBlock() != null) { ExprStatementsBlock e = s.attrNearestExprStatementsBlock(); - if (e.getReturnStmt() != s) { + if (!isClosureImplementationBlock(e) && e.getReturnStmt() != s) { s.addError("Return in a statements block can only be at the end."); return; } @@ -1863,6 +1898,11 @@ private void visit(StmtReturn s) { } } + private boolean isClosureImplementationBlock(ExprStatementsBlock block) { + Element parent = block.getParent(); + return parent instanceof ExprClosure && ((ExprClosure) parent).getImplementation() == block; + } + private void checkReturnInFunc(StmtReturn s, FunctionImplementation func) { WurstType returnType = func.attrReturnTyp(); if (s.getReturnedObj() instanceof Expr) { diff --git a/de.peeeq.wurstscript/src/test/java/de/peeeq/wurstio/languageserver/LanguageWorkerTest.java b/de.peeeq.wurstscript/src/test/java/de/peeeq/wurstio/languageserver/LanguageWorkerTest.java index 27d8aa2ba..3dad031fd 100644 --- a/de.peeeq.wurstscript/src/test/java/de/peeeq/wurstio/languageserver/LanguageWorkerTest.java +++ b/de.peeeq.wurstscript/src/test/java/de/peeeq/wurstio/languageserver/LanguageWorkerTest.java @@ -109,8 +109,7 @@ public void dependencyWatcherChangedSyncsIncrementally() throws Exception { new FileEvent(wFile.getUriString(), FileChangeType.Changed) ))); - assertTrue(waitUntil(() -> mm.refreshDependencyCalls.get() >= 1, 2000), "dependency watcher changes should refresh dependency roots"); - assertTrue(waitUntil(() -> mm.syncFileCalls.get() >= 1, 2000), "dependency watcher changes should sync changed file"); + assertTrue(waitUntil(() -> mm.syncDependencyCalls.get() >= 1, 2000), "dependency watcher changes should sync dependency state"); assertEquals(mm.cleanCalls.get(), 0, "dependency watcher changes should not clean"); assertEquals(mm.buildCalls.get(), 0, "dependency watcher changes should not full rebuild"); } finally { @@ -238,6 +237,7 @@ private static class CountingModelManager implements ModelManager { final AtomicInteger syncFileCalls = new AtomicInteger(); final AtomicInteger syncContentCalls = new AtomicInteger(); final AtomicInteger refreshDependencyCalls = new AtomicInteger(); + final AtomicInteger syncDependencyCalls = new AtomicInteger(); final AtomicInteger reconcileCalls = new AtomicInteger(); final File projectPath; @@ -274,6 +274,12 @@ public void refreshDependencies() { refreshDependencyCalls.incrementAndGet(); } + @Override + public Changes syncDependencyCompilationUnits() { + syncDependencyCalls.incrementAndGet(); + return Changes.empty(); + } + @Override public Changes syncCompilationUnit(WFile changedFilePath) { syncFileCalls.incrementAndGet(); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClosureTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClosureTests.java index 1881e50a3..dcf418cb2 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClosureTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClosureTests.java @@ -141,6 +141,51 @@ public void captureParam() { ); } + @Test + public void overloadCodeVsClosureInterface_prefersClosureInterface() { + testAssertOkLines(false, + "package test", + "interface IntCallback", + " function run(int p)", + "class Wrapper", + " function forEach(code callback)", + " skip", + " function forEach(IntCallback callback)", + " callback.run(1)", + "init", + " let w = new Wrapper()", + " var x = 0", + " w.forEach() p ->", + " x += p" + ); + } + + @Test + public void nestedCodeClosureCaptureIsValidationError() { + testAssertErrorsLines(false, "passed as code to .addAction() ->", + "package test", + "interface PerkApplyFunc", + " function run(int p)", + "interface VoidCallback", + " function run()", + "class TriggerWrap", + " function addAction(code c)", + " skip", + "native takesInt(int i)", + "function doAfter(real delay, VoidCallback cb)", + " skip", + "function addPerk(PerkApplyFunc f)", + " skip", + "init", + " let roundStartTrigger = new TriggerWrap()", + " addPerk((int plr) -> begin", + " roundStartTrigger.addAction() ->", + " doAfter(0.01) ->", + " takesInt(plr)", + " end)" + ); + } + @Test public void captureThis() { testAssertOkLines(true, @@ -342,6 +387,61 @@ public void closure_void_call() { ); } + @Test + public void closure_returnInsideIf_inStatementsBlock() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface FnBool", + " function run() returns boolean", + "function foo(FnBool fn) returns boolean", + " return fn.run()", + "init", + " FnBool fn = () ->", + " let i = 0", + " if i > 0", + " return false", + " return true", + " if foo(fn)", + " testSuccess()" + ); + } + + @Test + public void closure_multipleEarlyReturns_inStatementsBlock() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface FnInt", + " function run() returns int", + "function call(FnInt f) returns int", + " return f.run()", + "init", + " let x = call() ->", + " let i = 2", + " if i < 0", + " return -1", + " if i > 1", + " return 7", + " return 3", + " if x == 7", + " testSuccess()" + ); + } + + @Test + public void statementsBlockOutsideClosure_stillRequiresReturnAtEnd() { + testAssertErrorsLines(false, "Return in a statements block can only be at the end.", + "package test", + "init", + " let x = begin", + " if true", + " return 1", + " return 2", + " end" + ); + } + @Test public void code_anonfunc1() { testAssertOkLines(false, diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java index 8e029fe3a..90fda54d3 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/HotReloadPipelineTests.java @@ -58,6 +58,40 @@ public void hotReloadExtractionUsesSourceMap() throws Exception { assertEquals(Files.readString(scriptFile.toPath()), "source script"); } + @Test + public void cachedMapFileNameIsModeSpecific() throws Exception { + File projectFolder = new File("./temp/testProject_cache_mode_specific/"); + File wurstFolder = new File(projectFolder, "wurst"); + newCleanFolder(wurstFolder); + + File sourceMap = new File(projectFolder, "source_map.w3x"); + Files.write(sourceMap.toPath(), new byte[] {0x01}); + + WurstLanguageServer langServer = new WurstLanguageServer(); + TestMapRequest luaRequest = new TestMapRequest( + langServer, + Optional.of(sourceMap), + List.of("-lua"), + WFile.create(projectFolder), + Map.of() + ); + TestMapRequest jassRequest = new TestMapRequest( + langServer, + Optional.of(sourceMap), + List.of(), + WFile.create(projectFolder), + Map.of() + ); + + File luaCache = luaRequest.getCachedMapFileForTest(); + File jassCache = jassRequest.getCachedMapFileForTest(); + assertNotNull(luaCache); + assertNotNull(jassCache); + assertEquals(luaCache.equals(jassCache), false, "Lua/Jass modes must not share cached map filename"); + assertEquals(luaCache.getName().contains("_lua_cached.w3x"), true); + assertEquals(jassCache.getName().contains("_jass_cached.w3x"), true); + } + @Test public void jhcrPipelineRenamesOutputScript() throws Exception { File projectFolder = new File("./temp/testProject_jhcr_output/"); @@ -192,5 +226,9 @@ private File loadMapScriptForTest(Optional mapCopy, ModelManagerImpl model private File renameJhcrOutputForTest(File buildDir) throws Exception { return renameJhcrOutput(buildDir); } + + private File getCachedMapFileForTest() { + return getCachedMapFile(); + } } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java index a0e924011..f1633079b 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java @@ -547,6 +547,30 @@ public void inlayHintsStayStableWhileTemporarilyUnparsable() throws IOException assertTrue(errorLabels.contains("amount:"), "Expected cached hints to be reused during parse errors."); } + @Test + public void inlayHintsDoNotCrashOnCascadeCodeLambdaWithMissingTarget() throws IOException { + CompletionTestData data = input( + "package test", + "class TriggerWrap", + " function addAction(code c)", + " skip", + "init", + " let t = new TriggerWrap()", + " ..missing() ->", + " skip", + "endpackage" + ); + TestContext ctx = createContext(data, data.buffer); + + InlayHintParams params = new InlayHintParams( + new TextDocumentIdentifier(ctx.uri), + new Range(new Position(0, 0), new Position(100, 0)) + ); + + List hints = new InlayHintsRequest(params, ctx.bufferManager).execute(ctx.modelManager); + assertNotNull(hints); + } + @Test public void prepareRenameReturnsSymbolRange() throws IOException { CompletionTestData data = input( diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java index 4d0faff0d..3e9f816a1 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaNativesTests.java @@ -42,5 +42,34 @@ public void triggerEvaluateReturnsBoolInFallback() { assertTrue(rendered.contains("for i,a in ipairs(t.actions) do a() end")); assertTrue(rendered.contains("return true")); } -} + @Test + public void initHashtableCreatesPerTypeBuckets() { + String rendered = renderNative("__wurst_InitHashtable"); + assertTrue(rendered.contains("__wurst_ht_int")); + assertTrue(rendered.contains("__wurst_ht_bool")); + assertTrue(rendered.contains("__wurst_ht_real")); + assertTrue(rendered.contains("__wurst_ht_str")); + assertTrue(rendered.contains("__wurst_ht_handle")); + } + + @Test + public void hashtableSavesUseTypeSpecificBuckets() { + String saveInt = renderNative("__wurst_SaveInteger"); + String saveReal = renderNative("__wurst_SaveReal"); + String saveHandle = renderNative("__wurst_SaveAbilityHandle"); + assertTrue(saveInt.contains("h.__wurst_ht_int")); + assertTrue(saveReal.contains("h.__wurst_ht_real")); + assertTrue(saveHandle.contains("h.__wurst_ht_handle")); + } + + @Test + public void hashtableLoadsUseTypeSpecificBuckets() { + String loadInt = renderNative("__wurst_LoadInteger"); + String loadStr = renderNative("__wurst_LoadStr"); + String loadHandle = renderNative("__wurst_LoadAbilityHandle"); + assertTrue(loadInt.contains("h.__wurst_ht_int")); + assertTrue(loadStr.contains("h.__wurst_ht_str")); + assertTrue(loadHandle.contains("h.__wurst_ht_handle")); + } +} diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java index 4949b413e..0ab8e9976 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTranslationTests.java @@ -8,6 +8,8 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -63,6 +65,12 @@ private void assertDoesNotContainRegex(String output, String regex) { assertFalse("Pattern must not occur: " + regex, matcher.find()); } + private void assertContainsRegex(String output, String regex) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(output); + assertTrue("Pattern must occur: " + regex, matcher.find()); + } + @Test public void testStdLib() throws IOException { test().testLua(true).withStdLib().lines( @@ -73,6 +81,47 @@ public void testStdLib() throws IOException { ); String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_testStdLib.lua"), Charsets.UTF_8); assertTrue(compiled.contains("MagicFunctions_compiletime")); + assertTrue(compiled.contains("function __wurst_InitHashtable(")); + assertTrue(compiled.contains("function __wurst_SaveInteger(")); + assertTrue(compiled.contains("function __wurst_LoadInteger(")); + assertFunctionBodyContains(compiled, "__wurst_LoadInteger", "return 0", true); + } + + @Test + public void continueLoweringInLua() throws IOException { + test().testLua(true).lines( + "package Test", + "native testSuccess()", + "init", + " int i = 0", + " int sum = 0", + " while i < 5", + " i++", + " if i mod 2 == 0", + " continue", + " sum += i", + " if sum == 9", + " testSuccess()" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_continueLoweringInLua.lua"), Charsets.UTF_8); + assertTrue("expected continue lowering helper flag in lua output", compiled.contains("continueFlag_")); + } + + @Test + public void noContinueDoesNotEmitContinueFlagInLua() throws IOException { + test().testLua(true).lines( + "package Test", + "native testSuccess()", + "init", + " int i = 0", + " while i < 3", + " i++", + " if i > 10", + " skip", + " testSuccess()" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_noContinueDoesNotEmitContinueFlagInLua.lua"), Charsets.UTF_8); + assertFalse("continue flag helper should not be emitted when no continue is present", compiled.contains("continueFlag_")); } @Ignore @@ -200,6 +249,69 @@ public void stringConcatenation() throws IOException { assertFunctionBodyContains(compiled, "test", "stringConcat", true); } + @Test + public void numericOpsDoNotUseGlobalEnsureWrapping() throws IOException { + test().testLua(true).lines( + "package Test", + "function cmp(int a, int b) returns boolean", + " return a < b", + "function divi(int a, int b) returns int", + " return a div b", + "function addi(int a, int b) returns int", + " return a + b", + "init", + " cmp(1, 2)", + " divi(2, 1)", + " addi(1, 2)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_numericOpsDoNotUseGlobalEnsureWrapping.lua"), Charsets.UTF_8); + assertFunctionBodyContains(compiled, "cmp", "intEnsure", false); + assertFunctionBodyContains(compiled, "cmp", "realEnsure", false); + assertFunctionBodyContains(compiled, "divi", "intEnsure", false); + assertFunctionBodyContains(compiled, "divi", "realEnsure", false); + assertFunctionBodyContains(compiled, "addi", "intEnsure", false); + assertFunctionBodyContains(compiled, "addi", "realEnsure", false); + } + + @Test + public void lazyGenericClosureDispatchWorksInLua() throws IOException { + test().testLua(true).lines( + "package Test", + "native testSuccess()", + "public function lazy(Lazy l) returns Lazy", + " return l", + "public abstract class Lazy", + " T val = null", + " var wasRetrieved = false", + " abstract function retrieve() returns T", + " function get() returns T", + " if not wasRetrieved", + " val = retrieve()", + " wasRetrieved = true", + " return val", + "init", + " let l = lazy(() -> 5)", + " if l.get() == 5", + " testSuccess()" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_lazyGenericClosureDispatchWorksInLua.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("Lazy_lazy_Test.Lazy_retrieve =")); + assertTrue(compiled.contains("l:Lazy_get()")); + } + + @Test + public void stringArrayReadIsEnsured() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "string array playerName", + "init", + " let i = 0", + " SetPlayerName(Player(i), playerName[i])" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_stringArrayReadIsEnsured.lua"), Charsets.UTF_8); + assertContainsRegex(compiled, "SetPlayerName\\(Player\\([^\\)]*\\),\\s*stringEnsure\\("); + } + @Test public void methodFieldNameCollision() throws IOException { test().testLua(true).lines( @@ -349,6 +461,49 @@ public void oldGenericsCastingDoesNotUseGetHandleId() throws IOException { assertFunctionBodyContains(compiled, "testCast", "__wurst_objectFromIndex", true); } + @Test + public void newGenericsDoNotUseOldTypecastHelpersInLua() throws IOException { + test().testLua(true).lines( + "package Test", + "class C", + "function id(T x) returns T", + " return x", + "function testGeneric()", + " let c = new C()", + " let c2 = id(c)", + " if c2 == null", + " skip", + "init", + " testGeneric()" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_newGenericsDoNotUseOldTypecastHelpersInLua.lua"), Charsets.UTF_8); + assertFunctionBodyContains(compiled, "testGeneric", "__wurst_objectToIndex", false); + assertFunctionBodyContains(compiled, "testGeneric", "__wurst_objectFromIndex", false); + } + + @Test + public void largeFunctionSpillsLocalsIntoTableInLua() throws IOException { + List lines = new ArrayList<>(); + lines.add("package Test"); + lines.add("native takesInt(int i)"); + lines.add("function huge()"); + lines.add(" var sum = 0"); + for (int i = 0; i < 210; i++) { + lines.add(" let v" + i + " = " + i); + lines.add(" sum += v" + i); + } + lines.add(" takesInt(sum)"); + lines.add("init"); + lines.add(" huge()"); + + test().testLua(true).lines(lines.toArray(new String[0])); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_largeFunctionSpillsLocalsIntoTableInLua.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("function huge(")); + assertTrue(compiled.contains("__wurst_locals")); + assertFalse(compiled.contains("local v0")); + assertFalse(compiled.contains("local v209")); + } + @Test public void reflectionNativesStubbedForLua() throws IOException { test().testLua(true).lines( @@ -465,6 +620,180 @@ public void stdLibDoesNotEmitWar3HashtableNatives() throws IOException { assertDoesNotContainRegex(compiled, "\\bLoadReal\\("); assertDoesNotContainRegex(compiled, "\\bLoadStr\\("); assertDoesNotContainRegex(compiled, "\\bFlushChildHashtable\\("); + assertDoesNotContainRegex(compiled, "\\bHaveSavedHandle\\("); + assertDoesNotContainRegex(compiled, "\\bRemoveSavedHandle\\("); + assertDoesNotContainRegex(compiled, "\\bSaveAbilityHandle\\("); + assertDoesNotContainRegex(compiled, "\\bLoadAbilityHandle\\("); + assertTrue(compiled.contains("function __wurst_HaveSavedHandle(")); + } + + @Test + public void hashtableHandleExtensionsUseWurstLuaHelpers() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "import Hashtable", + "init", + " let h = InitHashtable()", + " if h.hasHandle(1, 2)", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHandleExtensionsUseWurstLuaHelpers.lua"), Charsets.UTF_8); + assertDoesNotContainRegex(compiled, "\\bHaveSavedHandle\\("); + assertContainsRegex(compiled, "\\b__wurst_HaveSavedHandle\\("); + assertTrue(compiled.contains("Wurst experimental Lua assertion guards")); + } + + @Test + public void hashtableHandleLoadSaveUseWurstLuaHelpers() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "import Hashtable", + "init", + " let h = InitHashtable()", + " h.saveAbilityHandle(1, 2, null)", + " let a = h.loadAbilityHandle(1, 2)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHandleLoadSaveUseWurstLuaHelpers.lua"), Charsets.UTF_8); + assertDoesNotContainRegex(compiled, "\\bSaveAbilityHandle\\("); + assertDoesNotContainRegex(compiled, "\\bLoadAbilityHandle\\("); + assertContainsRegex(compiled, "\\b__wurst_SaveAbilityHandle\\("); + assertContainsRegex(compiled, "\\b__wurst_LoadAbilityHandle\\("); + } + + @Test + public void hashtableHelpersEmitPerTypeBucketsInLua() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "import Hashtable", + "init", + " let h = InitHashtable()", + " h.saveInt(1, 2, 7)", + " h.saveReal(1, 2, 3.5)", + " h.saveString(1, 2, \"x\")", + " h.saveAbilityHandle(1, 2, null)", + " let i = h.loadInt(1, 2)", + " let r = h.loadReal(1, 2)", + " let s = h.loadString(1, 2)", + " let a = h.loadAbilityHandle(1, 2)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_hashtableHelpersEmitPerTypeBucketsInLua.lua"), Charsets.UTF_8); + assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_int", true); + assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_real", true); + assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_str", true); + assertFunctionBodyContains(compiled, "__wurst_InitHashtable", "__wurst_ht_handle", true); + assertFunctionBodyContains(compiled, "__wurst_SaveInteger", "h.__wurst_ht_int", true); + assertFunctionBodyContains(compiled, "__wurst_SaveReal", "h.__wurst_ht_real", true); + assertFunctionBodyContains(compiled, "__wurst_SaveStr", "h.__wurst_ht_str", true); + assertFunctionBodyContains(compiled, "__wurst_SaveAbilityHandle", "h.__wurst_ht_handle", true); + assertFunctionBodyContains(compiled, "__wurst_LoadInteger", "h.__wurst_ht_int", true); + assertFunctionBodyContains(compiled, "__wurst_LoadReal", "h.__wurst_ht_real", true); + assertFunctionBodyContains(compiled, "__wurst_LoadStr", "h.__wurst_ht_str", true); + assertFunctionBodyContains(compiled, "__wurst_LoadAbilityHandle", "h.__wurst_ht_handle", true); + } + + @Test + public void luaFunctionRefWrapperForwardsVarargs() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " let f = CreateForce()", + " ForForce(f, () -> skip)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_luaFunctionRefWrapperForwardsVarargs.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("xpcall(function (...)")); + assertTrue(compiled.contains(", ...)")); + assertFalse(compiled.contains("local temp = ...")); + assertFalse(compiled.contains("ForForce(f, function (...) \n\t\t\tlocal tempRes")); + } + + @Test + public void forForceIsRemappedToWurstHelperInLua() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " let f = CreateForce()", + " ForForce(f, () -> begin", + " if GetEnumPlayer() != null", + " skip", + " end)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_forForceIsRemappedToWurstHelperInLua.lua"), Charsets.UTF_8); + assertContainsRegex(compiled, "\\b__wurst_ForForce\\("); + assertContainsRegex(compiled, "\\b__wurst_GetEnumPlayer\\("); + assertContainsRegex(compiled, "\\bfunction\\s+__wurst_ForForce\\s*\\("); + assertContainsRegex(compiled, "\\bfunction\\s+__wurst_GetEnumPlayer\\s*\\("); + } + + @Test + public void nestedForForceUsesRemappedHelpersInLua() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "force g", + "function inner()", + " ForForce(g, () -> begin", + " if GetEnumPlayer() != null", + " skip", + " end)", + "init", + " g = CreateForce()", + " ForForce(g, () -> inner())" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_nestedForForceUsesRemappedHelpersInLua.lua"), Charsets.UTF_8); + assertContainsRegex(compiled, "\\bfunction\\s+__wurst_ForForce\\s*\\("); + assertContainsRegex(compiled, "\\bfunction\\s+__wurst_GetEnumPlayer\\s*\\("); + Matcher forForceCalls = Pattern.compile("\\b__wurst_ForForce\\s*\\(").matcher(compiled); + int count = 0; + while (forForceCalls.find()) { + count++; + } + assertTrue("expected at least two remapped __wurst_ForForce call sites for nested loops", count >= 2); + } + + @Test + public void wurstGetEnumPlayerPrefersNativeBeforeOverride() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " let f = CreateForce()", + " ForForce(f, () -> begin", + " if GetEnumPlayer() != null", + " skip", + " end)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_wurstGetEnumPlayerPrefersNativeBeforeOverride.lua"), Charsets.UTF_8); + assertContainsRegex(compiled, "function\\s+__wurst_GetEnumPlayer\\s*\\(\\)"); + assertContainsRegex(compiled, "if GetEnumPlayer ~= nil then\\s+local p = GetEnumPlayer\\(\\)\\s+if p ~= nil then return p end\\s+end\\s+if __wurst_enumPlayer_override ~= nil then return __wurst_enumPlayer_override end"); + } + + @Test + public void groupItemDestructableCallbacksUseWurstContextHelpersInLua() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "init", + " let g = CreateGroup()", + " ForGroup(g, () -> begin", + " if GetEnumUnit() != null", + " skip", + " end)", + " EnumItemsInRect(null, null, () -> begin", + " if GetEnumItem() != null", + " skip", + " end)", + " EnumDestructablesInRect(null, null, () -> begin", + " if GetEnumDestructable() != null", + " skip", + " end)" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_groupItemDestructableCallbacksUseWurstContextHelpersInLua.lua"), Charsets.UTF_8); + assertContainsRegex(compiled, "\\b__wurst_ForGroup\\("); + assertContainsRegex(compiled, "\\b__wurst_GetEnumUnit\\("); + assertContainsRegex(compiled, "\\b__wurst_EnumItemsInRect\\("); + assertContainsRegex(compiled, "\\b__wurst_GetEnumItem\\("); + assertContainsRegex(compiled, "\\b__wurst_EnumDestructablesInRect\\("); + assertContainsRegex(compiled, "\\b__wurst_GetEnumDestructable\\("); + assertContainsRegex(compiled, "\\bfunction\\s+__wurst_ForGroup\\s*\\("); + assertContainsRegex(compiled, "\\bfunction\\s+__wurst_EnumItemsInRect\\s*\\("); + assertContainsRegex(compiled, "\\bfunction\\s+__wurst_EnumDestructablesInRect\\s*\\("); } @Test @@ -497,4 +826,23 @@ public void luaErrorWrapperIgnoresAbortSentinel() throws IOException { assertTrue(compiled.contains("if err2 == \"__wurst_abort_thread\" then return end")); } + @Test + public void removesUnusedClassesFromLuaOutput() throws IOException { + test().testLua(true).lines( + "package Test", + "class Keep", + " function ping()", + " skip", + "class Drop", + " function pong()", + " skip", + "init", + " let k = new Keep()", + " k.ping()" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTranslationTests_removesUnusedClassesFromLuaOutput.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("Keep")); + assertFalse(compiled.contains("Drop")); + } + } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTypecastingTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTypecastingTests.java index 14094fdb7..fe9cd96c3 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTypecastingTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaTypecastingTests.java @@ -1,8 +1,15 @@ package tests.wurstscript.tests; +import com.google.common.base.Charsets; +import com.google.common.io.Files; import org.testng.annotations.Test; +import java.io.File; +import java.io.IOException; +import java.util.regex.Pattern; + +import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertTrue; @@ -199,4 +206,122 @@ public void compiletimeHashMapStringPutInLua() { ); } + @Test + public void compiletimeIterableMapStringPutInLua() { + test().testLua(true).withStdLib().lines( + "package Test", + "import HashMap", + "let modes = compiletime(new IterableMap())", + "@compiletime function initialize()", + " modes.put(\"r\", 1)", + " modes.put(\"sr\", 2)" + ); + } + + @Test + public void luaTypeCastingStringIndexUsesLuaMapping() { + test().testLua(true).withStdLib().lines( + "package Test", + "import TypeCasting", + "@compiletime function initialize()", + " let s = stringFromIndex(stringToIndex(\"abc\"))", + " if s == \"abc\"", + " skip" + ); + } + + @Test + public void luaFramehandleFromIndexDoesNotUseFogstateHashtablePath() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "import TypeCasting", + "init", + " let fh = framehandleFromIndex(1)", + " let idx = framehandleToIndex(fh)", + " let u = unitFromIndex(2)", + " let ui = unitToIndex(u)", + " if idx >= 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTypecastingTests_luaFramehandleFromIndexDoesNotUseFogstateHashtablePath.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("__wurst_objectFromIndex(")); + Pattern framehandleFromIndexUsesLuaHelper = Pattern.compile( + "function\\s+framehandleFromIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectFromIndex\\(", + Pattern.MULTILINE + ); + assertTrue(framehandleFromIndexUsesLuaHelper.matcher(compiled).find()); + Pattern unitFromIndexUsesLuaHelper = Pattern.compile( + "function\\s+unitFromIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectFromIndex\\(", + Pattern.MULTILINE + ); + assertTrue(unitFromIndexUsesLuaHelper.matcher(compiled).find()); + Pattern unitFromIndexSavesFog = Pattern.compile( + "function\\s+unitFromIndex\\([^)]*\\)[\\s\\S]*?Table_saveFogState[\\s\\S]*?\\nend", + Pattern.MULTILINE + ); + assertFalse(unitFromIndexSavesFog.matcher(compiled).find()); + } + + @Test + public void luaTypeCastingCompatWrappersUseLuaHelpers() throws IOException { + test().testLua(true).withStdLib().lines( + "package Test", + "import TypeCasting", + "init", + " let u = unitFromIndex(1)", + " let ui = unitToIndex(u)", + " let w = widgetFromIndex(2)", + " let wi = widgetToIndex(w)", + " let fh = framehandleFromIndex(3)", + " let fhi = framehandleToIndex(fh)", + " let k = oskeytypeFromIndex(4)", + " let ki = oskeytypeToIndex(k)", + " let s = stringFromIndex(stringToIndex(\"abc\"))", + " if ui + wi + fhi + ki >= 0 and s.length() >= 0", + " skip" + ); + String compiled = Files.toString(new File("test-output/lua/LuaTypecastingTests_luaTypeCastingCompatWrappersUseLuaHelpers.lua"), Charsets.UTF_8); + + assertTrue(Pattern.compile( + "function\\s+unitFromIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectFromIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + assertTrue(Pattern.compile( + "function\\s+unitToIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectToIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + assertTrue(Pattern.compile( + "function\\s+widgetFromIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectFromIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + assertTrue(Pattern.compile( + "function\\s+widgetToIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectToIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + assertTrue(Pattern.compile( + "function\\s+framehandleFromIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectFromIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + assertTrue(Pattern.compile( + "function\\s+framehandleToIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectToIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + assertTrue(Pattern.compile( + "function\\s+oskeytypeFromIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectFromIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + assertTrue(Pattern.compile( + "function\\s+oskeytypeToIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_objectToIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + assertTrue(Pattern.compile( + "function\\s+stringToIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_stringToIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + assertTrue(Pattern.compile( + "function\\s+stringFromIndex\\([^)]*\\)[\\s\\S]*?return\\s+__wurst_stringFromIndex\\(", + Pattern.MULTILINE + ).matcher(compiled).find()); + } + } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java index 2e1a6855c..b8ec87295 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java @@ -941,6 +941,57 @@ public void runmapPurge_onlyKeepsWar3MapFromProjectWurstFolder() throws Exceptio assertEquals(manager.getCompilationUnit(fileDependencyWar3Map), null, "dependency war3map.j must not be retained implicitly"); } + @Test + public void runmapPurge_configOverrideCacheWithDetachedDependencyPackagesDoesNotCrash() throws Exception { + File projectFolder = new File("./temp/testProject_runmap_purge_config_detached/"); + File wurstFolder = new File(projectFolder, "wurst"); + File dependencyRoot = new File(new File(new File(projectFolder, "_build"), "dependencies"), "depConfig"); + File dependencyWurst = new File(dependencyRoot, "wurst"); + newCleanFolder(wurstFolder); + newCleanFolder(dependencyWurst); + + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + WFile fileMain = WFile.create(new File(wurstFolder, "Main.wurst")); + WFile fileProjectWar3Map = WFile.create(new File(wurstFolder, "war3map.j")); + WFile fileDepBase = WFile.create(new File(dependencyWurst, "UpgradeObjEditing.wurst")); + WFile fileDepConfig = WFile.create(new File(dependencyWurst, "UpgradeObjEditing_config.wurst")); + + writeFile(fileWurst, "package Wurst\n"); + writeFile(fileMain, string( + "package Main", + "init", + " skip" + )); + writeFile(fileProjectWar3Map, "globals\nendglobals\n"); + writeFile(fileDepBase, string( + "package UpgradeObjEditing", + "@configurable int version = 1", + "endpackage" + )); + writeFile(fileDepConfig, string( + "package UpgradeObjEditing_config", + "@config int version = 2", + "endpackage" + )); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + manager.buildProject(); + manager.syncCompilationUnit(fileDepBase); + manager.syncCompilationUnit(fileDepConfig); + assertNotNull(manager.getCompilationUnit(fileDepBase), "dependency base package should be loaded"); + assertNotNull(manager.getCompilationUnit(fileDepConfig), "dependency config package should be loaded"); + + // Force config-override attribute cache before purge so removed packages can become detached later. + assertNotNull(manager.getModel().attrConfigOverridePackages()); + + purgeUnimportedFiles_likeRunMap(manager.getModel(), manager); + assertEquals(manager.getCompilationUnit(fileDepBase), null, "unimported dependency base package CU must be removed"); + assertEquals(manager.getCompilationUnit(fileDepConfig), null, "unimported dependency config package CU must be removed"); + + // Regression: translate must not crash with "package ... is not attached to a model". + runRunmapLikeCompile_Closer(projectFolder, manager); + } + @Test public void syncCompilationUnitContent_noopContentProducesNoChanges() throws Exception { File projectFolder = new File("./temp/testProject_sync_noop/"); @@ -965,6 +1016,68 @@ public void syncCompilationUnitContent_noopContentProducesNoChanges() throws Exc assertEquals(changes.isEmpty(), true, "unchanged sync should not trigger reconcile work"); } + @Test + public void dependencySyncHandlesReplaceMoveRenameDelete() throws Exception { + File projectFolder = new File("./temp/testProject_dependency_sync_changes/"); + File wurstFolder = new File(projectFolder, "wurst"); + File depAWurst = new File(new File(new File(new File(projectFolder, "_build"), "dependencies"), "depA"), "wurst"); + File depBWurst = new File(new File(new File(new File(projectFolder, "_build"), "dependencies"), "depB"), "wurst"); + newCleanFolder(wurstFolder); + newCleanFolder(depAWurst); + newCleanFolder(depBWurst); + + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); + WFile fileMain = WFile.create(new File(wurstFolder, "Main.wurst")); + WFile depAFile = WFile.create(new File(depAWurst, "DummyDamage.wurst")); + WFile depBFile = WFile.create(new File(depBWurst, "DummyDamageNew.wurst")); + WFile depBRenamed = WFile.create(new File(depBWurst, "DummyDamageRenamed.wurst")); + + writeFile(fileWurst, "package Wurst\n"); + writeFile(fileMain, string( + "package Main", + "import DummyDamage", + "init", + " foo()" + )); + writeFile(depAFile, string( + "package DummyDamage", + "public function foo()" + )); + + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); + Map errors = keepErrorsInMap(manager); + manager.buildProject(); + assertEquals(errors.get(fileMain), "", "baseline build with depA should be clean"); + assertNotNull(manager.getCompilationUnit(depAFile), "depA CU should be loaded"); + + // replace: depA is removed and depB provides the same package + depAFile.getFile().delete(); + writeFile(depBFile, string( + "package DummyDamage", + "public function foo()" + )); + ModelManager.Changes changes = manager.syncDependencyCompilationUnits(); + manager.reconcile(changes); + assertEquals(manager.getCompilationUnit(depAFile), null, "removed dependency CU must be dropped from model"); + assertNotNull(manager.getCompilationUnit(depBFile), "replacement dependency CU should be loaded"); + assertEquals(errors.get(fileMain), "", "replace should keep compilation clean"); + + // move/rename inside dependency folder + Files.move(depBFile.getFile().toPath(), depBRenamed.getFile().toPath()); + changes = manager.syncDependencyCompilationUnits(); + manager.reconcile(changes); + assertEquals(manager.getCompilationUnit(depBFile), null, "moved dependency source should be removed"); + assertNotNull(manager.getCompilationUnit(depBRenamed), "renamed dependency source should be loaded"); + assertEquals(errors.get(fileMain), "", "move/rename should keep compilation clean"); + + // delete dependency entirely + depBRenamed.getFile().delete(); + changes = manager.syncDependencyCompilationUnits(); + manager.reconcile(changes); + assertEquals(manager.getCompilationUnit(depBRenamed), null, "deleted dependency source should be removed"); + assertImportMissing(errors.getOrDefault(fileMain, ""), "DummyDamage"); + } + private void purgeUnimportedFiles_likeRunMap(WurstModel model, ModelManagerImpl manager) { java.util.Set keep = model.stream() .filter(cu -> isInProjectWurstFolder_likeRunMap(cu.getCuInfo().getFile(), manager) diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java index e47fb8a9d..7c3217590 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RealWorldExamples.java @@ -1,12 +1,5 @@ package tests.wurstscript.tests; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import de.peeeq.wurstio.WurstCompilerJassImpl; -import de.peeeq.wurstscript.RunArgs; -import de.peeeq.wurstscript.WLogger; -import de.peeeq.wurstscript.gui.WurstGuiCliImpl; -import de.peeeq.wurstscript.validation.GlobalCaches; import org.testng.annotations.Ignore; import org.testng.annotations.Test; @@ -15,7 +8,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; - public class RealWorldExamples extends WurstScriptTest { private static final String TEST_DIR = "./testscripts/concept/"; @@ -137,22 +129,11 @@ public void blubber() throws IOException { @Test public void test_stdlib() { - List inputs = Lists.newLinkedList(); - // TODO set config - RunArgs runArgs = RunArgs.defaults(); - runArgs.addLibs(Sets.newHashSet(StdLib.getLib())); - WurstCompilerJassImpl comp = new WurstCompilerJassImpl(null, new WurstGuiCliImpl(), null, runArgs); - for (File f : comp.getLibs().values()) { - WLogger.info("Adding file: " + f); - inputs.add(f); - } - new TestConfig("stdlib") .withStdLib(true) .executeTests(true) .executeProgOnlyAfterTransforms(false) .executeProg(false) - .withInputFiles(inputs) .run() .getModel(); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/SimpleStatementTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/SimpleStatementTests.java index 90539579f..7506b1aa8 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/SimpleStatementTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/SimpleStatementTests.java @@ -96,6 +96,28 @@ public void testWhileBreak() { ""); } + @Test + public void testContinueOutsideLoop() { + assertError(false, "Continue statements must be used inside a loop.", + "continue" + ); + } + + @Test + public void testWhileContinue() { + assertOk(true, + "int x = 0", + "int sum = 0", + "while x < 5", + " x++", + " if x mod 2 == 1", + " continue", + " sum += x", + "if sum == 6", + " testSuccess()", + ""); + } + @Test public void testFor1() { assertOk(true, @@ -129,6 +151,32 @@ public void testForStep() { ""); } + @Test + public void testForContinueUp() { + assertOk(true, + "int sum = 0", + "for int i = 1 to 6", + " if i mod 2 == 1", + " continue", + " sum += i", + "if sum == 12", + " testSuccess()", + ""); + } + + @Test + public void testForContinueDown() { + assertOk(true, + "int sum = 0", + "for int i = 6 downto 1", + " if i mod 2 == 0", + " continue", + " sum += i", + "if sum == 9", + " testSuccess()", + ""); + } + @Test public void testForDownStep() { assertOk(true, @@ -267,6 +315,48 @@ public void testForFrom3() { } + @Test + public void testForInContinue() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "class IntList", + " static int array elements", + " int size = 0", + " private function getOffset() returns int", + " return 64*((this castTo int)-1)", + " function add(int x) returns IntList", + " elements[getOffset() + size] = x", + " size++", + " return this", + " function get(int i) returns int", + " return elements[getOffset() + i]", + " function iterator() returns IntListIterator", + " return new IntListIterator(this)", + "class IntListIterator", + " IntList list", + " int pos = 0", + " construct(IntList list)", + " this.list = list", + " function hasNext() returns boolean", + " return pos < list.size", + " function next() returns int", + " pos++", + " return list.get(pos-1)", + " function close()", + " destroy this", + "init", + " IntList list = new IntList().add(1).add(2).add(3).add(4)", + " int sum = 0", + " for int i in list", + " if i mod 2 == 0", + " continue", + " sum += i", + " if sum == 4", + " testSuccess()" + ); + } + @Test public void testForFrom_once() { // for-from expression should be evaluated only once @@ -291,6 +381,29 @@ public void testForFrom_once() { } + @Test + public void testForFromContinue() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "class C", + " int x = 0", + " function next() returns int", + " x++", + " return x", + " function hasNext() returns boolean", + " return x < 5", + "init", + " int sum = 0", + " for i from new C()", + " if i mod 2 == 0", + " continue", + " sum += i", + " if sum == 9", + " testSuccess()" + ); + } + @Test public void test_inc() { assertOk(true, diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/VarargTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/VarargTests.java index f74a0c48f..d706301eb 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/VarargTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/VarargTests.java @@ -130,6 +130,24 @@ public void varargWithBreak() { ); } + @Test + public void varargWithContinue() { + testAssertErrorsLines(true, "Cannot use continue in vararg for each loops", + "package Test", + "native testSuccess()", + "function foo(vararg int ints)", + " var sum = 0", + " for i in ints", + " if i > 2", + " continue", + " sum += i", + " if sum == 3", + " testSuccess()", + "init", + " foo(1,2,3,4)" + ); + } + @Test public void legitNestedBreak() { testAssertOkLines(true,