diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fff7b43b6..d06e124f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,32 +1,97 @@ -name: Build Pipeline +name: CI (build & test) + on: push: - branches: - - master + branches: [ master ] pull_request: types: [opened, synchronize, reopened] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: - name: Build and analyze - runs-on: ubuntu-latest + name: Gradle build on ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + permissions: + contents: read + env: + GRADLE_OPTS: -Dorg.gradle.daemon=false + defaults: + run: + working-directory: de.peeeq.wurstscript + steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 17 - uses: actions/setup-java@v3 + fetch-depth: 0 + + - name: Setup Temurin JDK 25 + uses: actions/setup-java@v4 with: - java-version: 25 - distribution: 'zulu' # Alternative distribution options are available - - name: Cache Gradle packages - uses: actions/cache@v3 + distribution: temurin + java-version: '25' + cache: 'gradle' + + # Linux only: use a portable, pristine Temurin 25 for jlink + - name: (Linux) Install portable Temurin 25 + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + URL="https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25%2B36/OpenJDK25U-jdk_x64_linux_hotspot_25_36.tar.gz" + mkdir -p "$RUNNER_TEMP/temurin25" + curl -fsSL "$URL" -o "$RUNNER_TEMP/temurin25/jdk.tar.gz" + tar -xzf "$RUNNER_TEMP/temurin25/jdk.tar.gz" -C "$RUNNER_TEMP/temurin25" + PORTABLE_JAVA_HOME="$(find "$RUNNER_TEMP/temurin25" -maxdepth 1 -type d -name 'jdk-25*' | head -n1)" + echo "PORTABLE_JAVA_HOME=$PORTABLE_JAVA_HOME" >> "$GITHUB_ENV" + echo "$PORTABLE_JAVA_HOME/bin" >> "$GITHUB_PATH" + + # Pin Gradle toolchain to the active JDK (portable on Linux, setup-java on Windows) + - name: Pin Gradle toolchain + shell: bash + run: | + ACTIVE_JAVA_HOME="${PORTABLE_JAVA_HOME:-$JAVA_HOME}" + echo "JAVA_HOME=${ACTIVE_JAVA_HOME}" >> "$GITHUB_ENV" + echo "${ACTIVE_JAVA_HOME}/bin" >> "$GITHUB_PATH" + echo "org.gradle.java.installations.paths=${ACTIVE_JAVA_HOME}" >> gradle.properties + echo "org.gradle.java.installations.auto-detect=false" >> gradle.properties + + - name: Show Java & jlink + shell: bash + run: | + echo "JAVA_HOME=$JAVA_HOME" + "$JAVA_HOME/bin/java" -version + "$JAVA_HOME/bin/jlink" --version + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + + - name: Setup Gradle (cache) + uses: gradle/actions/setup-gradle@v4 + + # ---- FAIL FAST: package first (so jlink issues show immediately) ---- + - name: Package slim runtime (fail fast) + shell: bash + run: ./gradlew checksumSlimCompilerDist --no-daemon --stacktrace + + - name: Run tests + shell: bash + run: ./gradlew test --no-daemon --stacktrace + + - name: Upload packaged artifact (per-OS) + uses: actions/upload-artifact@v4 with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} - restore-keys: ${{ runner.os }}-gradle - - name: Build & Run Tests - working-directory: ./de.peeeq.wurstscript - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew test --info + name: wurst-compiler-${{ matrix.os }} + path: | + de.peeeq.wurstscript/build/releases/*.zip + de.peeeq.wurstscript/build/releases/*.tar.gz + de.peeeq.wurstscript/build/releases/*.sha256 + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a5798942..d69dffb59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,51 +1,65 @@ -name: Deploy Pipeline +name: Release slim runtimes (jlink) on: push: - branches: - - master + tags: + - 'v*' # e.g. v1.8.1.0 workflow_dispatch: jobs: - deploy: - name: Build WurstScript and Upload to GitHub Release - runs-on: ubuntu-latest + build: + name: Build ${{ matrix.plat }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + plat: win-x64 + - os: ubuntu-latest + plat: linux-x64 + - os: macos-13 + plat: macos-x64 + - os: macos-14 + plat: macos-arm64 + + runs-on: ${{ matrix.os }} + permissions: + contents: write + env: + GRADLE_OPTS: -Dorg.gradle.daemon=false + defaults: + run: + working-directory: de.peeeq.wurstscript steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 # your version task uses git describe - - name: Set up JDK 17 + - name: Setup Temurin JDK 25 uses: actions/setup-java@v4 with: - java-version: 17 distribution: temurin + java-version: '25' - - name: Grant Gradle permissions - working-directory: ./de.peeeq.wurstscript - run: chmod +x ./gradlew + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 - - name: Build WurstScript zips - working-directory: ./de.peeeq.wurstscript - run: ./gradlew create_zips + - name: Setup Gradle cache + uses: gradle/actions/setup-gradle@v4 - - name: Create or update nightly-master tag - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -f nightly-master - git push -f origin nightly-master + # Build fat jar + jdeps + jlink + package + checksum for this OS + - name: Package slim runtime (per-OS) + run: ./gradlew --no-daemon --stacktrace checksumSlimCompilerDist - - name: Create GitHub Release and upload zips - uses: softprops/action-gh-release@v1 + - name: Upload to GitHub Release + uses: softprops/action-gh-release@v2 with: - tag_name: nightly-master - name: Nightly Build (master) - body: "This release is automatically updated from the latest push to `master`." - draft: false - prerelease: true + # Attaches the per-OS files produced on this runner files: | - de.peeeq.wurstscript/build/distributions/wurstpack_complete.zip - de.peeeq.wurstscript/build/distributions/wurstpack_compiler.zip + de.peeeq.wurstscript/build/releases/wurst-compiler-*-*.zip + de.peeeq.wurstscript/build/releases/wurst-compiler-*-*.tar.gz + de.peeeq.wurstscript/build/releases/*.sha256 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/de.peeeq.wurstscript/build.gradle b/de.peeeq.wurstscript/build.gradle index 293088f0d..4f3a2f1d3 100644 --- a/de.peeeq.wurstscript/build.gradle +++ b/de.peeeq.wurstscript/build.gradle @@ -34,8 +34,11 @@ application { version = "1.8.1.0" java { - toolchain { languageVersion = JavaLanguageVersion.of(25) } + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } } + tasks.withType(JavaCompile).configureEach { options.release = 25 } jacoco { diff --git a/de.peeeq.wurstscript/deploy.gradle b/de.peeeq.wurstscript/deploy.gradle index 3900d12f0..0d00ed817 100644 --- a/de.peeeq.wurstscript/deploy.gradle +++ b/de.peeeq.wurstscript/deploy.gradle @@ -1,4 +1,6 @@ +import org.gradle.internal.os.OperatingSystem +// ----------------------- Publishing (kept from your original) ----------------------- publishing { publications { mavenJava(MavenPublication) { @@ -14,3 +16,221 @@ publishing { // or: maven { url = uri("${buildDir}/repo") } } } + + +def toolJavaHome = { + def svc = project.extensions.getByType(JavaToolchainService) + def launcher = svc.launcherFor(project.java.toolchain) + launcher.get().metadata.installationPath.asFile +} + +def toolExecutable = { String toolName -> + def javaHome = System.getenv('JAVA_HOME') ? new File(System.getenv('JAVA_HOME')) : toolJavaHome() + def ext = OperatingSystem.current().isWindows() ? ".exe" : "" + new File(javaHome, "bin/${toolName}${ext}").absolutePath +} + + +// ----------------------- Common providers/locations --------------------------------- +def fatJar = tasks.named('shadowJar').flatMap { it.archiveFile } + +// Allow adding modules if reflection/ServiceLoader pulls them in: +// ./gradlew jlinkRuntime25 -PextraJdkModules=java.desktop,jdk.crypto.ec +def extraJdkModules = (project.findProperty("extraJdkModules") ?: "").toString().trim() + +def jlinkWorkDir = layout.buildDirectory.dir("jlink") +def modulesTxt = jlinkWorkDir.map { it.file("modules.txt") } +def jreImageDir = layout.buildDirectory.dir("jre-wurst-25") + +def os = OperatingSystem.current() +def arch = System.getProperty("os.arch") // "amd64"/"x86_64"/"aarch64" +def plat = os.isWindows() ? "win-x64" + : os.isMacOsX() ? (arch == "aarch64" ? "macos-arm64" : "macos-x64") + : "linux-x64" + +def distRoot = layout.buildDirectory.dir("dist/slim-${plat}") +def releasesDir = layout.buildDirectory.dir("releases") + +// ----------------------- Tasks: jdeps → jlink → assemble → package ------------------ + +// 1) Detect JDK modules via jdeps (from the fat jar) +tasks.register("jdepsModules") { + description = "Detects required JDK modules for the fat compiler.jar via jdeps" + group = "distribution" + + inputs.file(fatJar) + outputs.file(modulesTxt) + + doLast { + ExecOperations execOps = project.services.get(ExecOperations) + def jdeps = toolExecutable("jdeps") + def outBuf = new ByteArrayOutputStream() + + modulesTxt.get().asFile.parentFile.mkdirs() + + execOps.exec { + commandLine jdeps, + "--multi-release", "25", + "--ignore-missing-deps", + "--print-module-deps", + fatJar.get().asFile.absolutePath + standardOutput = outBuf + } + + def detected = outBuf.toString().trim() + if (detected.isEmpty()) detected = "java.base" + if (!extraJdkModules.isEmpty()) detected = detected + "," + extraJdkModules + + modulesTxt.get().asFile.text = detected + logger.lifecycle("[jdeps] Using modules: ${detected}") + } +} + +// 2) Build slim runtime with jlink (overwrite if exists) +// 2) Build slim runtime with jlink (overwrite if exists) +tasks.register("jlinkRuntime25") { + description = "Builds a slim Java 25 runtime image containing only the needed modules" + group = "distribution" + dependsOn("shadowJar", "jdepsModules") + + inputs.file(modulesTxt) + outputs.dir(jreImageDir) + + doLast { + ExecOperations execOps = project.services.get(ExecOperations) + + // Resolve Java home from toolchain (fallback to JAVA_HOME if set) + def svc = project.extensions.getByType(JavaToolchainService) + def launcher = svc.launcherFor(project.java.toolchain) + File javaHome = System.getenv('JAVA_HOME') ? new File(System.getenv('JAVA_HOME')) : launcher.get().metadata.installationPath.asFile + + def jlink = new File(javaHome, "bin/${OperatingSystem.current().isWindows() ? 'jlink.exe' : 'jlink'}").absolutePath + def jmodsDir = new File(javaHome, "jmods").absolutePath + def mods = modulesTxt.get().asFile.text.trim() + def outDir = jreImageDir.get().asFile + + if (!mods) throw new GradleException("No modules detected for jlink.") + + // jlink requires the output dir to NOT exist + if (outDir.exists()) { + project.delete(outDir) + } + outDir.parentFile.mkdirs() + + logger.lifecycle("[jlink] Using: ${jlink}") + logger.lifecycle("[jlink] JAVA_HOME: ${javaHome}") + logger.lifecycle("[jlink] jmods: ${jmodsDir}") + logger.lifecycle("[jlink] Modules: ${mods}") + + def errBuf = new ByteArrayOutputStream() + def outBuf = new ByteArrayOutputStream() + + def result = execOps.exec { + commandLine jlink, + "--verbose", + "--module-path", jmodsDir, + "--add-modules", mods, + "--no-header-files", + "--no-man-pages", + "--strip-debug", + "--compress=zip-6", + "--output", outDir.absolutePath + errorOutput = errBuf + standardOutput = outBuf + ignoreExitValue = true + } + + if (result.exitValue != 0) { + logger.lifecycle("[jlink][stdout]\n${outBuf.toString()}") + logger.error("[jlink][stderr]\n${errBuf.toString()}") + throw new GradleException("jlink failed with exit ${result.exitValue}") + } + + logger.lifecycle("[jlink] Runtime created at: ${outDir}") + } +} + + + +// 3) Assemble folder layout: jre + compiler.jar (no manifest) +tasks.register("assembleSlimCompilerDist", Copy) { + description = "Assembles dist folder with slim JRE and compiler.jar (no manifest)." + group = "distribution" + dependsOn("jlinkRuntime25", "shadowJar") + + from(jreImageDir) { into("wurst-runtime") } + from(fatJar) { into("wurst-compiler") } + into(distRoot) + + doLast { + logger.lifecycle("[dist] Folder ready at: ${distRoot.get().asFile}") + } +} + +// 4a) Package ZIP on Windows +tasks.register("packageSlimCompilerDistZip", Zip) { + description = "Packages slim dist as a ZIP archive (Windows)." + group = "distribution" + enabled = os.isWindows() + + dependsOn("assembleSlimCompilerDist") + + from(distRoot) + destinationDirectory.set(releasesDir) + archiveFileName.set("wurst-compiler-${project.version}-${plat}.zip") +} + +// 4b) Package tar.gz on Linux/macOS +tasks.register("packageSlimCompilerDistTar", Tar) { + description = "Packages slim dist as a tar.gz archive (Linux/macOS)." + group = "distribution" + enabled = !os.isWindows() + + dependsOn("assembleSlimCompilerDist") + + from(distRoot) + destinationDirectory.set(releasesDir) + compression = Compression.GZIP + archiveExtension.set("tar.gz") + archiveFileName.set("wurst-compiler-${project.version}-${plat}.tar.gz") +} + +// 4c) OS-aware convenience wrapper +tasks.register("packageSlimCompilerDist") { + description = "Packages slim dist for the current platform (zip on Windows, tar.gz elsewhere)." + group = "distribution" + dependsOn(os.isWindows() ? "packageSlimCompilerDistZip" : "packageSlimCompilerDistTar") +} + +// 5) SHA-256 checksum for the packaged archive +tasks.register("checksumSlimCompilerDist") { + description = "Writes SHA-256 alongside the packaged archive." + group = "distribution" + dependsOn("packageSlimCompilerDist") + + outputs.upToDateWhen { false } + + doLast { + def outDir = releasesDir.get().asFile + def expectedZip = new File(outDir, "wurst-compiler-${project.version}-${plat}.zip") + def expectedTarGz = new File(outDir, "wurst-compiler-${project.version}-${plat}.tar.gz") + def archFile = expectedZip.exists() ? expectedZip : (expectedTarGz.exists() ? expectedTarGz : null) + if (archFile == null) throw new GradleException("Archive not found in ${outDir}") + + def md = java.security.MessageDigest.getInstance("SHA-256") + archFile.withInputStream { is -> + byte[] buf = new byte[8192] + for (int r = is.read(buf); r != -1; r = is.read(buf)) { + md.update(buf, 0, r) + } + } + def hex = md.digest().collect { String.format("%02x", it) }.join() + def sumFile = new File(outDir, archFile.name + ".sha256") + sumFile.text = "${hex} ${archFile.name}\n" + + logger.lifecycle("[sha256] ${sumFile.name} -> ${hex}") + logger.lifecycle("[release] Upload: ${archFile.absolutePath}") + } +} + + diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java index 483869e61..542294e4c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/ProgramStateIO.java @@ -430,54 +430,75 @@ public void writeBack(boolean inject) { gui.sendProgress("Writing back generated objects"); long startTime = System.currentTimeMillis(); - // Load the existing cache manifest ObjectCacheManifest oldManifest = loadObjectCacheManifest(); ObjectCacheManifest newManifest = new ObjectCacheManifest(); - int filesProcessed = 0; - int filesUpdated = 0; - int filesSkipped = 0; + int filesProcessed = 0, filesUpdated = 0, filesSkipped = 0; + boolean anyFileWritten = false; for (ObjectFileType fileType : ObjectFileType.values()) { filesProcessed++; ObjMod dataStore = getDataStore(fileType); - if (!dataStore.getObjsList().isEmpty()) { - // Calculate hash of current object file - String currentHash = calculateObjectFileHash(dataStore); - dataStoreHashes.put(fileType, currentHash); + if (dataStore.getObjsList().isEmpty()) { + WLogger.info("Object file " + fileType.getExt() + " is empty, skipping"); + continue; + } - // Check if it matches the cached version - if (oldManifest.hasEntry(fileType.getExt()) && - oldManifest.hashMatches(fileType.getExt(), currentHash)) { + // Compute hash of what we intend to write + String currentHash = calculateObjectFileHash(dataStore); + dataStoreHashes.put(fileType, currentHash); + + boolean mpqHasSame = false; + if (mpqEditor != null && mpqEditor.hasFile("war3map." + fileType.getExt())) { + try { + byte[] existing = mpqEditor.extractFile("war3map." + fileType.getExt()); + String existingHash = ImportFile.calculateHash(existing); + mpqHasSame = existingHash.equals(currentHash); + } catch (Exception e) { + WLogger.info("Could not validate MPQ content for " + fileType.getExt() + ": " + e.getMessage()); + } + } - System.out.println("Object file " + fileType.getExt() + " unchanged (hash match), skipping writeback"); - filesSkipped++; + boolean manifestSaysSame = oldManifest.hasEntry(fileType.getExt()) + && oldManifest.hashMatches(fileType.getExt(), currentHash); - // Still add to new manifest - newManifest.putEntry(fileType.getExt(), currentHash, dataStore.getObjsList().size()); - } else { - System.out.println("Object file " + fileType.getExt() + " changed or new, writing back"); - filesUpdated++; - writebackObjectFile(dataStore, fileType, inject); + // Only skip if BOTH the manifest and the actual MPQ content match + if (manifestSaysSame && mpqHasSame) { + WLogger.info("Object file " + fileType.getExt() + " unchanged (hash match), skipping writeback"); + filesSkipped++; + if (inject) { newManifest.putEntry(fileType.getExt(), currentHash, dataStore.getObjsList().size()); } - } else { - WLogger.info("Object file " + fileType.getExt() + " is empty, skipping"); + continue; + } + + WLogger.info("Object file " + fileType.getExt() + " changed or MPQ out of sync, writing back"); + filesUpdated++; + writebackObjectFile(dataStore, fileType, inject); + anyFileWritten = anyFileWritten || inject; + if (inject) { + newManifest.putEntry(fileType.getExt(), currentHash, dataStore.getObjsList().size()); } } - // Always write w3o file (it's relatively cheap) - writeW3oFile(); + // Always write the .w3o (debug aid) – but do NOT touch the manifest for it +// writeW3oFile(); - // Save the new manifest - saveObjectCacheManifest(newManifest); + // Only persist a new manifest if we actually injected files + if (inject && anyFileWritten) { + saveObjectCacheManifest(newManifest); + } else { + WLogger.info("Skipping manifest update (inject=" + inject + ", anyFileWritten=" + anyFileWritten + ")"); + } long endTime = System.currentTimeMillis(); - WLogger.info(String.format("Object writeback complete in %dms: %d files processed, %d updated, %d skipped (cached)", + WLogger.info(String.format( + "Object writeback complete in %dms: %d files processed, %d updated, %d skipped", endTime - startTime, filesProcessed, filesUpdated, filesSkipped)); } + private void writeW3oFile() { Optional objFile = getObjectEditingOutputFolder().map(fo -> new File(fo, "wurstCreatedObjects.w3o")); try (Wc3BinOutputStream objFileStream = new Wc3BinOutputStream(objFile.get())) { @@ -516,6 +537,7 @@ private void writebackObjectFile(ObjMod dataStore, ObjectF String filenameInMpq = "war3map." + fileType.getExt(); mpqEditor.deleteFile(filenameInMpq); mpqEditor.insertFile(filenameInMpq, w3_); + WLogger.info("Injected modified object file: " + filenameInMpq); } } catch (Exception e) { WLogger.severe(e); 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 047afdf28..2ece3cba6 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 @@ -42,7 +42,7 @@ public String toString() { private final Thread thread; public final Duration reconcileWaitTime; - private ModelManager modelManager; + protected ModelManager modelManager; public void setRootPath(WFile rootPath) { this.rootPath = rootPath; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/WurstTextDocumentService.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/WurstTextDocumentService.java index 74f220264..643689c9f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/WurstTextDocumentService.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/WurstTextDocumentService.java @@ -105,6 +105,10 @@ public CompletableFuture resolveCodeLens(CodeLens unresolved) { @Override public CompletableFuture> formatting(DocumentFormattingParams params) { WLogger.info("formatting"); + + if (worker.modelManager.hasErrors()) { + throw new RequestFailedException(MessageType.Error, "Fix errors in your code before running.\n" + worker.modelManager.getFirstErrorDescription()); + } TextDocumentIdentifier doc = params.getTextDocument(); String buffer = worker.getBufferManager().getBuffer(doc); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/BuildMap.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/BuildMap.java index f3559607e..6ac3f4304 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/BuildMap.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/BuildMap.java @@ -15,6 +15,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.List; import java.util.Optional; @@ -66,6 +67,8 @@ public Object execute(ModelManager modelManager) throws IOException { injectMapData(gui, targetMap, result); + Files.copy(getCachedMapFile().toPath(), targetMap.get().toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + gui.sendProgress("Finalizing map"); try (MpqEditor mpq = MpqEditorFactory.getEditor(targetMap)) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/MpqEditor.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/MpqEditor.java index 2e467c6c8..cda5e6c79 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/MpqEditor.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/mpq/MpqEditor.java @@ -16,7 +16,7 @@ public interface MpqEditor extends Closeable { void deleteFile(String filenameInMpq) throws Exception; - boolean hasFile(String fileName) throws Exception; + boolean hasFile(String fileName); void setKeepHeaderOffset(boolean flag); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerDefault.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerDefault.java index bc2e0067c..552f5f440 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerDefault.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/WLoggerDefault.java @@ -35,8 +35,8 @@ public void debug(String s) { @Override public void info(String msg) { logger.info(msg); - if (System.currentTimeMillis() - startTime > 250) { // Wait 250 mseconds - System.out.println("Info: " + msg); + if (System.currentTimeMillis() - startTime > 100) { // Wait 250 mseconds +// System.out.println("Info: " + msg); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ErrorHandler.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ErrorHandler.java index 05f9f3157..ce3a180ef 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ErrorHandler.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ErrorHandler.java @@ -19,7 +19,7 @@ public class ErrorHandler { private final WurstGui gui; private boolean unitTestMode = false; - public static boolean outputTestSource = false; + public static boolean outputTestSource = true; public ErrorHandler(WurstGui gui) { this.gui = gui; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstReal.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstReal.java index 19c5aa4ec..e92b9aa30 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstReal.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/ILconstReal.java @@ -34,7 +34,7 @@ public ILconstNum negate() { return create(-val); } - static ILconstReal create(float f) { + public static ILconstReal create(float f) { return new ILconstReal(f); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java index 64e6caf0f..9aa489c72 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java @@ -12,7 +12,9 @@ import org.eclipse.jdt.annotation.Nullable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -29,7 +31,44 @@ public static ILconst eval(ImFuncRef e, ProgramState globalState, LocalState loc public static @Nullable ILconst eval(ImFunctionCall e, ProgramState globalState, LocalState localState) { ImFunction f = e.getFunc(); ImExprs arguments = e.getArguments(); - return evaluateFunc(globalState, localState, f, arguments, e); + + + // Evaluate arguments + ILconst[] args = new ILconst[arguments.size()]; + for (int i = 0; i < arguments.size(); i++) { + args[i] = arguments.get(i).evaluate(globalState, localState); + } + + Map typeSubstitutions = new HashMap<>(); + @Nullable ILconstObject receiver = null; + if (e.getFunc().getParent() != null) { + Element parent = e.getFunc().getParent().getParent(); + if (parent instanceof ImClass) { + ImTypeVars typeParams = ((ImClass) parent).getTypeVariables(); // The T74 parameters + ImTypeArguments typeArgs = e.getTypeArguments(); // The arguments + + // Create mapping: T74 -> integer + for (int i = 0; i < typeParams.size() && i < typeArgs.size(); i++) { + ImTypeVar genericParam = typeParams.get(i); + ImType concreteArg = typeArgs.get(i).getType(); + typeSubstitutions.put(genericParam, concreteArg); + } + + if (args.length > 0 && args[0] instanceof ILconstObject) { + receiver = (ILconstObject) args[0]; + } + + } + } + + globalState.pushStackframeWithTypes(f, receiver, args, e.attrTrace().attrErrorPos(), typeSubstitutions); + + + try { + return ILInterpreter.runFunc(globalState, f, e, args).getReturnVal(); + } finally { + globalState.popStackframe(); + } } public static @Nullable ILconst evaluateFunc(ProgramState globalState, @@ -52,15 +91,12 @@ public static ILconst eval(ImIntVal e, ProgramState globalState, LocalState loca } public static ILconst eval(ImNull e, ProgramState globalState, LocalState localState) { - if (e.getType() instanceof ImAnyType - || e.getType() instanceof ImClassType - || e.getType() instanceof ImTypeVarRef - || TypesHelper.isIntType(e.getType())) { - return ILconstInt.create(0); - } - return ILconstNull.instance(); + // Resolve generics / type variables according to the current frame (pre-elimination run). + ImType resolved = globalState.resolveType(e.getType()); + return resolved.defaultValue(); } + public static ILconst eval(ImOperatorCall e, final ProgramState globalState, final LocalState localState) { final ImExprs arguments = e.getArguments(); WurstOperator op = e.getOp(); @@ -179,34 +215,44 @@ public static ILconst eval(ImVarArrayAccess e, ProgramState globalState, LocalSt public static @Nullable ILconst eval(ImMethodCall mc, ProgramState globalState, LocalState localState) { ILconstObject receiver = globalState.toObject(mc.getReceiver().evaluate(globalState, localState)); - globalState.assertAllocated(receiver, mc.attrTrace()); - List args = mc.getArguments(); - ImMethod mostPrecise = mc.getMethod(); - - // find correct implementation: for (ImMethod m : mc.getMethod().getSubMethods()) { - if (m.attrClass().isSubclassOf(mostPrecise.attrClass())) { if (globalState.isInstanceOf(receiver, m.attrClass(), mc.attrTrace())) { - // found more precise method mostPrecise = m; } } } - // execute most precise method + ILconst[] eargs = new ILconst[args.size() + 1]; eargs[0] = receiver; for (int i = 0; i < args.size(); i++) { eargs[i + 1] = args.get(i).evaluate(globalState, localState); } - return evaluateFunc(globalState, mostPrecise.getImplementation(), mc, eargs); + + // NEW: push type substitutions for the method's owning class + ImFunction impl = mostPrecise.getImplementation(); + Map typeSubstitutions = new HashMap<>(); + ImTypeVars typeParams = mostPrecise.getMethodClass().getClassDef().getTypeVariables(); + ImTypeArguments typeArgs = mc.getTypeArguments(); // injected earlier by addMemberTypeArguments() + + for (int i = 0; i < typeParams.size() && i < typeArgs.size(); i++) { + typeSubstitutions.put(typeParams.get(i), typeArgs.get(i).getType()); + } + + globalState.pushStackframeWithTypes(impl, receiver, eargs, mc.attrTrace().attrErrorPos(), typeSubstitutions); + try { + return evaluateFunc(globalState, impl, mc, eargs); + } finally { + globalState.popStackframe(); + } } + public static ILconst eval(ImMemberAccess ma, ProgramState globalState, LocalState localState) { ILconstObject receiver = globalState.toObject(ma.getReceiver().evaluate(globalState, localState)); if (receiver == null) { @@ -220,9 +266,15 @@ public static ILconst eval(ImMemberAccess ma, ProgramState globalState, LocalSta return receiver.get(ma.getVar(), indexes).orElseGet(() -> ma.attrTyp().defaultValue()); } - public static ILconst eval(ImAlloc imAlloc, ProgramState globalState, - LocalState localState) { - return globalState.allocate(imAlloc.getClazz(), imAlloc.attrTrace()); + public static ILconst eval(ImAlloc e, ProgramState globalState, LocalState localState) { + // Get the generic type from the allocation instruction + ImClassType genericType = e.getClazz(); // This is Box + + // NEW: Resolve it using current stack frame's type substitutions + ImClassType concreteType = (ImClassType) globalState.resolveType(genericType); // This becomes Box + + // Allocate with the concrete type + return globalState.allocate(concreteType, e.attrTrace()); } public static ILconst eval(ImDealloc imDealloc, ProgramState globalState, diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java index cd3df9c7d..a0f2046df 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java @@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.Nullable; import java.io.File; +import java.util.*; import java.util.Arrays; import java.util.Optional; import java.util.stream.Collectors; @@ -72,15 +73,13 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab args[i] = adjustTypeOfConstant(args[i], f.getParameters().get(i).getType()); } - if (isCompiletimeNative(f)) { - return runBuiltinFunction(globalState, f, args); - } - - if (f.isNative()) { + if (isCompiletimeNative(f) || f.isNative()) { return runBuiltinFunction(globalState, f, args); } LocalState localState = new LocalState(); + + // Set up local variables int i = 0; for (ImVar p : f.getParameters()) { localState.setVal(p, args[i]); @@ -93,7 +92,44 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab globalState.setLastStatement(f.getBody().get(0)); } - globalState.pushStackframe(f, args, (caller == null ? f : caller).attrTrace().attrErrorPos()); + + if (!(caller instanceof ImFunctionCall)) { + if (caller instanceof ImMethodCall) { + // Instance method call: bind class T-vars from the *receiver*'s concrete type args + final Map subst = new HashMap<>(); + + // First parameter is the implicit 'this' + final ImVar thisParam = f.getParameters().get(0); + final ImType thisParamType = thisParam.getType(); + if (!(thisParamType instanceof ImClassType)) { + // Defensive: still push with no substitutions + globalState.pushStackframeWithTypes(f, null, args, f.attrTrace().attrErrorPos(), Collections.emptyMap()); + } else { + final ImClassType sigThisType = (ImClassType) thisParamType; // may contain ImTypeVarRefs + final ILconstObject thisArg = (ILconstObject) args[0]; + final ImClassType recvType = thisArg.getType(); // concrete type Box> etc. + + // Class type variables (on the class definition) + final ImClass cls = sigThisType.getClassDef(); + final ImTypeVars tvars = cls.getTypeVariables(); // e.g., [T74] + + // Concrete type arguments from receiver (same order) + final ImTypeArguments concreteArgs = recvType.getTypeArguments(); + + final int n = Math.min(tvars.size(), concreteArgs.size()); + for (int i2 = 0; i2 < n; i2++) { + subst.put(tvars.get(i2), concreteArgs.get(i2).getType()); + } + + globalState.pushStackframeWithTypes(f, thisArg, args, f.attrTrace().attrErrorPos(), subst); + } + } else { + // Static function or unknown caller kind + globalState.pushStackframeWithTypes(f, null, args, f.attrTrace().attrErrorPos(), Collections.emptyMap()); + } + } + + try { f.getBody().runStatements(globalState, localState); @@ -104,10 +140,12 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab retVal = adjustTypeOfConstant(retVal, f.getReturnType()); return localState.setReturnVal(retVal); } + if (f.getReturnType() instanceof ImVoid) { return localState; } throw new InterpreterException("function " + f.getName() + " did not return any value..."); + } catch (InterpreterException e) { String msg = buildStacktrace(globalState, e); e.setStacktrace(msg); @@ -229,8 +267,7 @@ private static LocalState runBuiltinFunction(ProgramState globalState, ImFunctio if (f.getReturnType() instanceof ImVoid) { return EMPTY_LOCAL_STATE; } - final ILconst returnValue = ImHelper.defaultValueForComplexType(f.getReturnType()) - .evaluate(globalState, EMPTY_LOCAL_STATE); + final ILconst returnValue = f.getReturnType().defaultValue(); return new LocalState(returnValue); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILStackFrame.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILStackFrame.java index 0fa8d4569..eb00c2b38 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILStackFrame.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILStackFrame.java @@ -2,29 +2,38 @@ import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.intermediatelang.ILconst; -import de.peeeq.wurstscript.jassIm.ImCompiletimeExpr; -import de.peeeq.wurstscript.jassIm.ImFunction; +import de.peeeq.wurstscript.intermediatelang.ILconstObject; +import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.parser.WPos; import io.vavr.control.Either; +import javax.annotation.Nullable; import java.io.File; +import java.util.Collections; +import java.util.Map; public class ILStackFrame { public final Either f; public final ILconst[] args; public final WPos trace; + public final @Nullable ILconstObject receiver; + public final Map typeSubstitutions; - public ILStackFrame(ImFunction f, ILconst[] args2, WPos trace) { + public ILStackFrame(ImFunction f, @Nullable ILconstObject receiver, ILconst[] args2, WPos trace, Map typeSubstitutions) { this.f = Either.left(f); + this.receiver = receiver; this.args = args2; this.trace = trace; + this.typeSubstitutions = typeSubstitutions; } public ILStackFrame(ImCompiletimeExpr f, WPos trace) { this.f = Either.right(f); this.args = new ILconst[0]; this.trace = trace; + this.receiver = null; + this.typeSubstitutions = Collections.emptyMap(); } public String getMessage() { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java index 03e3fab1b..8c7bdfd45 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java @@ -1,5 +1,7 @@ package de.peeeq.wurstscript.intermediatelang.interpreter; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import de.peeeq.wurstio.jassinterpreter.InterpreterException; import de.peeeq.wurstscript.ast.Element; @@ -12,6 +14,7 @@ import de.peeeq.wurstscript.utils.Utils; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.eclipse.jdt.annotation.Nullable; import java.io.PrintStream; import java.util.*; @@ -132,16 +135,161 @@ private ImClass findClazz(Object key) { throw new Error("no class found for key " + key); } - public void pushStackframe(ImFunction f, ILconst[] args, WPos trace) { - stackFrames.push(new ILStackFrame(f, args, trace)); - de.peeeq.wurstscript.jassIm.Element stmt = this.lastStatement; - if (stmt == null) { - stmt = f; + + public void pushStackframeWithTypes(ImFunction f, @Nullable ILconstObject receiver, + ILconst[] args, WPos trace, + Map typeSubstitutions) { + + // normalize the incoming map: RHS = fully resolved type + Map normalized = new HashMap<>(); + for (Map.Entry e : typeSubstitutions.entrySet()) { + ImType rhs = resolveType(e.getValue()); // resolve through existing frames + // skip self-maps (T -> T) + if (rhs instanceof ImTypeVarRef && + ((ImTypeVarRef) rhs).getTypeVariable() == e.getKey()) { + continue; + } + normalized.put(e.getKey(), rhs); } + +// System.out.println("pushStackframe " + f + " with receiver " + receiver +// + " and args " + Arrays.toString(args) + " and typesubst " + normalized); + + stackFrames.push(new ILStackFrame(f, receiver, args, trace, normalized)); + de.peeeq.wurstscript.jassIm.Element stmt = this.lastStatement; + if (stmt == null) stmt = f; lastStatements.push(stmt); } + + // NEW: Resolve a generic type using current stack frame's type substitutions +// Replace both resolveType(...) and substituteTypeVars(...) with this: + + public ImType resolveType(ImType t) { + return resolveTypeDeep(t, 32); // small budget to avoid cycles + } + + private ImType resolveTypeDeep(ImType t, int budget) { + if (budget <= 0 || t == null) return t; + + return t.match(new ImType.Matcher() { + @Override + public ImType case_ImTypeVarRef(ImTypeVarRef tvr) { + // search from top-most frame down + for (ILStackFrame sf : stackFrames) { /* we iterate deque top-first: use an iterator if needed */ + ImType mapped = sf.typeSubstitutions.get(tvr.getTypeVariable()); + if (mapped != null) { + ImType resolved = resolveTypeDeep(mapped, budget - 1); + return resolved; + } + } + // no mapping anywhere -> keep the type var + return tvr; + } + + @Override + public ImType case_ImClassType(ImClassType ct) { + if (ct.getTypeArguments().isEmpty()) return ct; + ImTypeArguments newArgs = JassIm.ImTypeArguments(); + boolean changed = false; + for (ImTypeArgument ta : ct.getTypeArguments()) { + ImType rt = resolveTypeDeep(ta.getType(), budget - 1); + newArgs.add(JassIm.ImTypeArgument(rt, ta.getTypeClassBinding())); + changed |= (rt != ta.getType()); + } + return changed ? JassIm.ImClassType(ct.getClassDef(), newArgs) : ct; + } + + @Override + public ImType case_ImArrayType(ImArrayType at) { + ImType et = resolveTypeDeep(at.getEntryType(), budget - 1); + return et == at.getEntryType() ? at : JassIm.ImArrayType(et); + } + + @Override + public ImType case_ImArrayTypeMulti(ImArrayTypeMulti at) { + ImType et = resolveTypeDeep(at.getEntryType(), budget - 1); + return et == at.getEntryType() ? at : JassIm.ImArrayTypeMulti(et, at.getArraySize()); + } + + @Override + public ImType case_ImTupleType(ImTupleType tt) { + List nts = new ArrayList<>(); + List names = new ArrayList<>(); + boolean changed = false; + var i = 0; + for (ImType it : tt.getTypes()) { + ImType rt = resolveTypeDeep(it, budget - 1); + nts.add(rt); + changed |= (rt != it); + i++; + names.add(i + ""); + } + return changed ? JassIm.ImTupleType(nts, names) : tt; + } + + @Override public ImType case_ImSimpleType(ImSimpleType st) { return st; } + @Override public ImType case_ImAnyType(ImAnyType at) { return at; } + @Override public ImType case_ImVoid(ImVoid v) { return v; } + }); + } + + + // Helper method to substitute type variables + private ImType substituteTypeVars(ImType type, Map substitutions) { + return type.match(new ImType.Matcher() { + @Override + public ImType case_ImTypeVarRef(ImTypeVarRef typeVarRef) { + ImType concrete = substitutions.get(typeVarRef.getTypeVariable()); + return concrete != null ? concrete : typeVarRef; + } + + @Override + public ImType case_ImClassType(ImClassType classType) { + // Recursively substitute in type arguments + ImTypeArguments newArgs = JassIm.ImTypeArguments(); + for (ImTypeArgument arg : classType.getTypeArguments()) { + ImType substituted = substituteTypeVars(arg.getType(), substitutions); + newArgs.add(JassIm.ImTypeArgument(substituted, arg.getTypeClassBinding())); + } + return JassIm.ImClassType(classType.getClassDef(), newArgs); + } + + // For other types, return as-is + @Override + public ImType case_ImSimpleType(ImSimpleType t) { + return t; + } + + @Override + public ImType case_ImArrayType(ImArrayType t) { + return t; + } + + @Override + public ImType case_ImTupleType(ImTupleType t) { + return t; + } + + @Override + public ImType case_ImVoid(ImVoid t) { + return t; + } + + @Override + public ImType case_ImAnyType(ImAnyType t) { + return t; + } + + @Override + public ImType case_ImArrayTypeMulti(ImArrayTypeMulti t) { + return t; + } + }); + } + public void pushStackframe(ImCompiletimeExpr f, WPos trace) { +// System.out.println("pushStackframe compiletime expr " + f); stackFrames.push(new ILStackFrame(f, trace)); de.peeeq.wurstscript.jassIm.Element stmt = this.lastStatement; if (stmt == null) { @@ -151,6 +299,7 @@ public void pushStackframe(ImCompiletimeExpr f, WPos trace) { } public void popStackframe() { +// System.out.println("popStackframe " + (stackFrames.isEmpty() ? "empty" : stackFrames.peek().f)); if (!stackFrames.isEmpty()) { stackFrames.pop(); } @@ -216,23 +365,81 @@ public void appendTo(StringBuilder sb) { } } - @Override public String toString() { + @Override + public String toString() { StringBuilder sb = new StringBuilder(stackFrames.size() * 32); appendTo(sb); return sb.toString(); } - public List getStackFrames() { return stackFrames; } + public List getStackFrames() { + return stackFrames; + } public Iterable getStackFramesReversed() { return () -> new Iterator() { int i = stackFrames.size() - 1; - @Override public boolean hasNext() { return i >= 0; } - @Override public ILStackFrame next() { return stackFrames.get(i--); } + + @Override + public boolean hasNext() { + return i >= 0; + } + + @Override + public ILStackFrame next() { + return stackFrames.get(i--); + } }; } } + private final Object2ObjectOpenHashMap genericStaticVals = new Object2ObjectOpenHashMap<>(); + + + public String instantiationKey(ImClassType ct) { + StringBuilder sb = new StringBuilder(); + sb.append(ct.getClassDef().getName()); + if (!ct.getTypeArguments().isEmpty()) { + sb.append('<'); + boolean first = true; + for (ImTypeArgument ta : ct.getTypeArguments()) { + if (!first) sb.append(','); + first = false; + sb.append(ta.toString()); // or your own stable printer + } + sb.append('>'); + } + return sb.toString(); + } + + @Override + public void setVal(ImVar v, ILconst val) { + if (v.isGlobal() && v.getType() instanceof ImTypeVarRef) { + ILStackFrame top = stackFrames.peek(); + if (top != null && top.receiver != null) { + ImTypeArguments tas = top.receiver.getType().getTypeArguments(); + ImType resolved = resolveType(tas.get(0).getType()); // <<< resolve + String s = v.getName() + resolved; + genericStaticVals.put(s, val); + return; + } + } + super.setVal(v, val); + } + + public @Nullable ILconst getVal(ImVar v) { + if (v.isGlobal() && v.getType() instanceof ImTypeVarRef) { + System.out.println("looking for generic static var " + v); + ILStackFrame top = stackFrames.peek(); + if (top != null && top.receiver != null) { + ImTypeArguments tas = top.receiver.getType().getTypeArguments(); + ImType resolved = resolveType(tas.get(0).getType()); // <<< resolve + String s = v.getName() + resolved; + return genericStaticVals.get(s); + } + } + return super.getVal(v); + } public boolean isCompiletime() { return isCompiletime; @@ -264,8 +471,6 @@ protected ILconstArray getArray(ImVar v) { } - - public Collection getAllObjects() { return indexToObject.values(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java index b418fc128..cce66cc9a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/RunStatement.java @@ -56,7 +56,8 @@ public static void run(ImReturn s, ProgramState globalState, LocalState localSta } public static void run(ImSet s, ProgramState globalState, LocalState localState) { - ILaddress v = s.getLeft().evaluateLvalue(globalState, localState); + ImLExpr left = s.getLeft(); + ILaddress v = left.evaluateLvalue(globalState, localState); ILconst right = s.getRight().evaluate(globalState, localState); v.set(right); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/State.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/State.java index 0d71b214a..6757fee07 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/State.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/State.java @@ -1,12 +1,10 @@ package de.peeeq.wurstscript.intermediatelang.interpreter; +import com.google.common.base.Objects; import de.peeeq.wurstio.jassinterpreter.InterpreterException; import de.peeeq.wurstscript.intermediatelang.ILconst; import de.peeeq.wurstscript.intermediatelang.ILconstArray; -import de.peeeq.wurstscript.jassIm.ImArrayLikeType; -import de.peeeq.wurstscript.jassIm.ImArrayTypeMulti; -import de.peeeq.wurstscript.jassIm.ImType; -import de.peeeq.wurstscript.jassIm.ImVar; +import de.peeeq.wurstscript.jassIm.*; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import org.eclipse.jdt.annotation.Nullable; @@ -22,22 +20,19 @@ public abstract class State { private @Nullable Object2ObjectOpenHashMap values; private @Nullable Object2ObjectOpenHashMap arrayValues; + private Object2ObjectOpenHashMap ensureValues() { - Object2ObjectOpenHashMap v = values; - if (v == null) { - v = new Object2ObjectOpenHashMap<>(8); - values = v; + if (values == null) { + values = new Object2ObjectOpenHashMap<>(8); } - return v; + return values; } protected Object2ObjectOpenHashMap ensureArrayValues() { - Object2ObjectOpenHashMap a = arrayValues; - if (a == null) { - a = new Object2ObjectOpenHashMap<>(4); - arrayValues = a; + if (arrayValues == null) { + arrayValues = new Object2ObjectOpenHashMap<>(4); } - return a; + return arrayValues; } @@ -50,7 +45,9 @@ public void setVal(ImVar v, ILconst val) { return vmap == null ? null : vmap.get(v); } - /** Returns the (lazy) array object for variable v, allocating only when first accessed. */ + /** + * Returns the (lazy) array object for variable v, allocating only when first accessed. + */ protected ILconstArray getArray(ImVar v) { Map amap = ensureArrayValues(); ILconstArray arr = amap.get(v); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/DefaultValue.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/DefaultValue.java index c78300d5e..af83e8283 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/DefaultValue.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/DefaultValue.java @@ -33,7 +33,7 @@ public static ILconst get(ImTupleType tt) { } public static ILconst get(ImVoid t) { - throw new Error("Could not get default value for void variable."); + return new ILconstInt(0); } public static ILconst get(ImArrayTypeMulti t) { @@ -57,6 +57,6 @@ public static ILconst get(ImClassType ct) { } public static ILconst get(ImAnyType imAnyType) { - return ILconstNull.instance(); + return new ILconstInt(0); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ExprTranslation.java index c13d15b9c..b1c88e0da 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtojass/ExprTranslation.java @@ -47,10 +47,34 @@ public static JassExpr translate(ImIntVal e, ImToJassTranslator translator) { } public static JassExpr translate(ImNull e, ImToJassTranslator translator) { - if (e.getType() instanceof ImAnyType - || TypesHelper.isIntType(e.getType())) { + ImType type = e.getType(); + + // Handle simple types (after EliminateGenerics specialization) + if (type instanceof ImSimpleType simpleType) { + String typename = simpleType.getTypename(); + + if (typename.equals("integer") || typename.equals("int")) { + return JassExprIntVal("0"); + } else if (typename.equals("real")) { + return JassExprRealVal("0."); + } else if (typename.equals("boolean") || typename.equals("bool")) { + return JassExprBoolVal(false); + } else if (typename.equals("string")) { + return JassExprStringVal(""); + } + } + + // Handle AnyType and int-like types + if (type instanceof ImAnyType || TypesHelper.isIntType(type)) { return JassExprIntVal("0"); } + + // Class types (reference types) can be null, represented as 0 in Jass + if (type instanceof ImClassType) { + return JassExprIntVal("0"); + } + + // Default: actual null (for handle types, etc.) return JassExprNull(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java index 294934283..10e9218f0 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java @@ -1,9 +1,7 @@ package de.peeeq.wurstscript.translation.imtranslation; -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Table; +import com.google.common.collect.*; +import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.translation.imtojass.ImAttrType; @@ -12,7 +10,6 @@ import java.util.*; import java.util.function.BiConsumer; -import java.util.stream.Collectors; /** * eliminate classes and dynamic method invocations @@ -21,13 +18,20 @@ public class EliminateGenerics { private final ImTranslator translator; private final ImProg prog; - // TODO only use one queue here with the different cases (add: generic class type, member access) private final Deque genericsUses = new ArrayDeque<>(); private final Table specializedFunctions = HashBasedTable.create(); private final Table specializedMethods = HashBasedTable.create(); private final Table specializedClasses = HashBasedTable.create(); private final Multimap> onSpecializedClassTriggers = HashMultimap.create(); + // NEW: Track specialized global variables for generic static fields + // Key: (original generic global var, concrete type instantiation) -> specialized var + private final Table specializedGlobals = HashBasedTable.create(); + + // NEW: Track which global vars belong to which generic class + // This helps us know which globals need specialization + private final Map globalToClass = new HashMap<>(); + public EliminateGenerics(ImTranslator tr, ImProg prog) { translator = tr; this.prog = prog; @@ -40,6 +44,9 @@ public void transform() { addMemberTypeArguments(); + // NEW: Identify generic globals before collecting usages + identifyGenericGlobals(); + collectGenericUsages(); eliminateGenericUses(); @@ -106,11 +113,11 @@ private static Iterable superTypes(ImClassType ct) { GenericTypes generics = new GenericTypes(ct.getTypeArguments()); List typeVars = ct.getClassDef().getTypeVariables(); return () -> - ct.getClassDef() - .getSuperClasses() - .stream() - .map(sc -> (ImClassType) transformType(sc, generics, typeVars)) - .iterator(); + ct.getClassDef() + .getSuperClasses() + .stream() + .map(sc -> (ImClassType) transformType(sc, generics, typeVars)) + .iterator(); } @@ -156,6 +163,37 @@ private void moveFunctionsOutOfClass(ImClass c) { } } + /** + * NEW: Identify global variables that belong to generic classes + * These are the "static" fields that need specialization + */ + private void identifyGenericGlobals() { + // Build a map of class name to class for quick lookup + Map classMap = new HashMap<>(); + for (ImClass c : prog.getClasses()) { + classMap.put(c.getName(), c); + } + + // Check each global variable to see if it belongs to a generic class + for (ImVar global : prog.getGlobals()) { + // Global variable names for static fields follow the pattern: ClassName_fieldName + String varName = global.getName(); + int underscoreIdx = varName.indexOf('_'); + if (underscoreIdx > 0) { + String potentialClassName = varName.substring(0, underscoreIdx); + ImClass owningClass = classMap.get(potentialClassName); + + if (owningClass != null && !owningClass.getTypeVariables().isEmpty()) { + // This global belongs to a generic class + if (containsTypeVariable(global.getType())) { + globalToClass.put(global, owningClass); + WLogger.info("Identified generic global: " + varName + " of type " + global.getType() + + " belonging to class " + owningClass.getName()); + } + } + } + } + } /** * When everything is specialized, we can remove generic functions and classes @@ -167,6 +205,15 @@ private void removeGenericConstructs() { for (ImClass c : prog.getClasses()) { c.getFields().removeIf(f -> isGenericType(f.getType())); } + + // NEW: Remove original generic global variables + prog.getGlobals().removeIf(v -> { + if (globalToClass.containsKey(v)) { + WLogger.info("Removing generic global variable: " + v.getName() + " with type " + v.getType()); + return true; + } + return false; + }); } private void eliminateGenericUses() { @@ -262,9 +309,9 @@ private void adaptSubmethods(List oldSubMethods, ImMethod newM) { private void rewriteGenerics(Element element, GenericTypes generics, List typeVars) { if (generics.getTypeArguments().size() != typeVars.size()) { throw new RuntimeException("Rewrite generics with wrong sizes\n" + - "generics: " + generics + "\n" + - "typevars: " + typeVars + "\n" + - "in\n: " + element); + "generics: " + generics + "\n" + + "typevars: " + typeVars + "\n" + + "in\n: " + element); } element.accept(new Element.DefaultVisitor() { @@ -358,13 +405,57 @@ private ImClass specializeClass(ImClass c, GenericTypes generics) { List typeVars = c.getTypeVariables(); rewriteGenerics(newC, generics, typeVars); newC.getSuperClasses().replaceAll(this::specializeType); - // we don't collect generic usages to avoid infinite loops - // in cases like class C { C> x; } + + // NEW: Create specialized global variables for this class instantiation + createSpecializedGlobals(c, generics, typeVars); + onSpecializedClassTriggers.get(c).forEach(consumer -> - consumer.accept(generics, newC)); + consumer.accept(generics, newC)); return newC; } + /** + * NEW: Create specialized global variables for each generic static field + */ + private void createSpecializedGlobals(ImClass originalClass, GenericTypes generics, List typeVars) { + // Find all global variables that belong to this class + for (Map.Entry entry : globalToClass.entrySet()) { + ImVar originalGlobal = entry.getKey(); + ImClass owningClass = entry.getValue(); + + if (owningClass != originalClass) { + continue; + } + + // Check if we already created this specialized version + if (specializedGlobals.contains(originalGlobal, generics)) { + continue; + } + + // Transform the type using the concrete generics + ImType specializedType = transformType(originalGlobal.getType(), generics, typeVars); + + // Create new global variable with specialized type + String specializedName = originalGlobal.getName() + "⟪" + generics.makeName() + "⟫"; + ImVar specializedGlobal = JassIm.ImVar( + originalGlobal.getTrace(), + specializedType, + specializedName, + originalGlobal.getIsBJ() + ); + + // Add to program globals + prog.getGlobals().add(specializedGlobal); + + // Track the specialization + specializedGlobals.put(originalGlobal, generics, specializedGlobal); + + WLogger.info("Created specialized global: " + specializedName + + " with type " + specializedType + + " for generics " + generics); + } + } + /** * Collects all usages from non-generic functions @@ -397,16 +488,53 @@ public void visit(ImMemberAccess ma) { if (!ma.getTypeArguments().isEmpty()) { genericsUses.add(new GenericMemberAccess(ma)); } + } + // NEW: Collect variable accesses to generic globals + @Override + public void visit(ImVarAccess va) { + super.visit(va); + if (globalToClass.containsKey(va.getVar())) { + genericsUses.add(new GenericGlobalAccess(va)); + } } + // NEW: Collect array accesses to generic globals + @Override + public void visit(ImVarArrayAccess vaa) { + super.visit(vaa); + if (globalToClass.containsKey(vaa.getVar())) { + genericsUses.add(new GenericGlobalArrayAccess(vaa)); + } + } + + // NEW: Collect assignments to generic globals + @Override + public void visit(ImSet set) { + super.visit(set); + if (set.getLeft() instanceof ImVarAccess) { + ImVarAccess va = (ImVarAccess) set.getLeft(); + if (globalToClass.containsKey(va.getVar())) { + genericsUses.add(new GenericGlobalAccess(va)); + } + } else if (set.getLeft() instanceof ImVarArrayAccess) { + ImVarArrayAccess vaa = (ImVarArrayAccess) set.getLeft(); + if (globalToClass.containsKey(vaa.getVar())) { + genericsUses.add(new GenericGlobalArrayAccess(vaa)); + } + } + } @Override public void visit(ImVar v) { super.visit(v); + + // Skip globals - they're handled elsewhere + if (v.isGlobal()) return; + + // Do NOT error on type variables here. The initializer/method calls may + // still specialize this. We'll validate at the very end. + // If it's generic-but-concrete, schedule specialization: if (isGenericType(v.getType())) { - if (containsTypeVariable(v.getType())) { - throw new CompileError(v, "Var should not have type variables."); - } genericsUses.add(new GenericVar(v)); } } @@ -648,6 +776,229 @@ public void eliminate() { } } + /** + * NEW: Handle accesses to generic global variables (static fields) + */ + class GenericGlobalAccess implements GenericUse { + private final ImVarAccess va; + + GenericGlobalAccess(ImVarAccess va) { + this.va = va; + } + + @Override + public void eliminate() { + ImVar originalGlobal = va.getVar(); + ImClass owningClass = globalToClass.get(originalGlobal); + + if (owningClass == null) { + WLogger.info("Warning: No owning class found for global " + originalGlobal.getName()); + return; + } + + // Infer the concrete type from the enclosing function + GenericTypes concreteGenerics = inferGenericsFromFunction(va, owningClass); + + if (concreteGenerics == null) { + WLogger.info("Warning: Could not infer generics for global access: " + originalGlobal.getName()); + return; + } + + // Get the specialized global variable + ImVar specializedGlobal = specializedGlobals.get(originalGlobal, concreteGenerics); + + if (specializedGlobal == null) { + WLogger.info("Warning: No specialized global found for " + originalGlobal.getName() + + " with generics " + concreteGenerics); + return; + } + + WLogger.info("Redirecting access from " + originalGlobal.getName() + + " to " + specializedGlobal.getName()); + + // Redirect to the specialized variable + va.setVar(specializedGlobal); + } + } + + /** + * NEW: Handle array accesses to generic global variables (static arrays) + */ + class GenericGlobalArrayAccess implements GenericUse { + private final ImVarArrayAccess vaa; + + GenericGlobalArrayAccess(ImVarArrayAccess vaa) { + this.vaa = vaa; + } + + @Override + public void eliminate() { + ImVar originalGlobal = vaa.getVar(); + ImClass owningClass = globalToClass.get(originalGlobal); + + if (owningClass == null) { + WLogger.info("Warning: No owning class found for global " + originalGlobal.getName()); + return; + } + + // Infer the concrete type from the enclosing function + GenericTypes concreteGenerics = inferGenericsFromFunction(vaa, owningClass); + + if (concreteGenerics == null) { + WLogger.info("Warning: Could not infer generics for global array access: " + originalGlobal.getName()); + return; + } + + // Get the specialized global variable + ImVar specializedGlobal = specializedGlobals.get(originalGlobal, concreteGenerics); + + if (specializedGlobal == null) { + WLogger.info("Warning: No specialized global found for " + originalGlobal.getName() + + " with generics " + concreteGenerics); + return; + } + + WLogger.info("Redirecting array access from " + originalGlobal.getName() + + " to " + specializedGlobal.getName()); + + // Redirect to the specialized variable + vaa.setVar(specializedGlobal); + } + } + + /** + * NEW: Infer generic types from the enclosing function context + * For specialized functions, the name contains the type information + */ + private GenericTypes inferGenericsFromFunction(Element element, ImClass owningClass) { + Element current = element; + while (current != null) { + if (current instanceof ImFunction) { + ImFunction func = (ImFunction) current; + + // If function is still generic, we can't decide yet. + if (!func.getTypeVariables().isEmpty()) { + return null; + } + + if (!func.getParameters().isEmpty()) { + ImVar receiver = func.getParameters().get(0); + ImType rt = receiver.getType(); + + if (rt instanceof ImClassType) { + ImClassType ct = (ImClassType) rt; + ImClass raw = ct.getClassDef(); + + boolean matches = + raw.getName().equals(owningClass.getName()) || + raw.getName().startsWith(owningClass.getName() + "⟪") || + raw.isSubclassOf(owningClass); + + if (matches) { + // PRIMARY: use actual type args if present + if (!ct.getTypeArguments().isEmpty()) { + List copied = new ArrayList<>(ct.getTypeArguments().size()); + for (ImTypeArgument ta : ct.getTypeArguments()) { + copied.add(JassIm.ImTypeArgument(ta.getType().copy(), ta.getTypeClassBinding())); + } + return new GenericTypes(copied); + } + + // FALLBACK: parse from specialized class name: Box⟪...⟫ + GenericTypes fromName = extractGenericsFromClassName(raw.getName()); + if (fromName != null && !fromName.getTypeArguments().isEmpty()) { + return fromName; + } + } + } + } + return null; + } + current = current.getParent(); + } + return null; + } + + + /** + * NEW: Extract generic types from a specialized class name like "Box⟪integer⟫" + */ + private GenericTypes extractGenericsFromClassName(String className) { + int start = className.indexOf('⟪'); + int end = className.lastIndexOf('⟫'); + if (start < 0 || end < 0 || end <= start + 1) return null; + + String payload = className.substring(start + 1, end).trim(); + List parts = splitTopLevel(payload); // comma-split with bracket depth + List args = new ArrayList<>(parts.size()); + for (String p : parts) { + ImType t = parseTypeAtom(p.trim()); + args.add(JassIm.ImTypeArgument(t, Collections.emptyMap())); + } + return new GenericTypes(args); + } + + /** split by commas at top level, respecting both ⟪⟫ and ⦅⦆ */ + private List splitTopLevel(String s) { + List res = new ArrayList<>(); + StringBuilder cur = new StringBuilder(); + int depthAngle = 0, depthTuple = 0; + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + if (ch == '⟪') depthAngle++; + else if (ch == '⟫') depthAngle--; + else if (ch == '⦅') depthTuple++; + else if (ch == '⦆') depthTuple--; + + if (ch == ',' && depthAngle == 0 && depthTuple == 0) { + res.add(cur.toString()); + cur.setLength(0); + continue; + } + cur.append(ch); + } + if (cur.length() > 0) res.add(cur.toString()); + return res; + } + + /** parse simple atoms and tuples like ⦅integer, integer⦆ (can nest) */ + private ImType parseTypeAtom(String s) { + s = s.trim(); + // tuple + if (s.startsWith("⦅") && s.endsWith("⦆")) { + String inner = s.substring(1, s.length() - 1).trim(); + List elems = splitTopLevel(inner); + List tt = new ArrayList<>(); + List names = Lists.newArrayList(); + int i = 1; + for (String e : elems) { + tt.add(parseTypeAtom(e)); + names.add("" + i++); + } + return JassIm.ImTupleType(tt, names); + } + + // common simples + switch (s) { + case "integer": + case "int": return JassIm.ImSimpleType("integer"); + case "real": return JassIm.ImSimpleType("real"); + case "boolean": + case "bool": return JassIm.ImSimpleType("boolean"); + case "string": return JassIm.ImSimpleType("string"); + } + + // class type without visible args here + for (ImClass c : prog.getClasses()) { + if (c.getName().equals(s)) { + return JassIm.ImClassType(c, JassIm.ImTypeArguments()); + } + } + // fallback: simple type with this name + return JassIm.ImSimpleType(s); + } + + class GenericVar implements GenericUse { private final ImVar mc; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java index 2eee14720..0bdf0dd1e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateTuples.java @@ -156,6 +156,28 @@ private static ImType getFirstType(ImType t) { private static void toTupleExpressions(ImStmts body, ImTranslator translator, ImFunction f) { Replacer replacer = new Replacer(); body.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImNull n) { + // Expand null<⦅T1, T2, ...⦆> ==> , null, ...> + ImType t = n.getType(); // or n.attrTyp() if that's the established source of truth + if (t instanceof ImTupleType) { + ImTupleType tt = (ImTupleType) t; + + ImExprs parts = JassIm.ImExprs(); + for (ImType elemT : tt.getTypes()) { + parts.add(JassIm.ImNull(elemT.copy())); + } + + ImTupleExpr replacement = JassIm.ImTupleExpr(parts); + // Replace node in-place: + Replacer replacer = new Replacer(); + replacer.replace(n, replacement); + } else { + super.visit(n); + } + } + + @Override public void visit(ImVarAccess va) { if (va.attrTyp() instanceof ImTupleType) { @@ -403,52 +425,169 @@ private static ImStatementExpr inSet(ImSet imSet, ImFunction f) { if (!(imSet.getLeft() instanceof ImTupleExpr && imSet.getRight() instanceof ImTupleExpr)) { throw new RuntimeException("invalid set statement:\n" + imSet); } - ImTupleExpr left = (ImTupleExpr) imSet.getLeft(); + ImTupleExpr left = (ImTupleExpr) imSet.getLeft(); ImTupleExpr right = (ImTupleExpr) imSet.getRight(); ImStmts stmts = JassIm.ImStmts(); - // 1) extract side effects from left expressions - List leftExprs = new ArrayList<>(); - for (ImExpr expr : left.getExprs()) { - leftExprs.add(extractSideEffect(expr, stmts)); + // 1) Flatten LHS into L-values (recursively), hoisting side-effects + List lhsLeaves = new ArrayList<>(); + for (ImExpr e : left.getExprs()) { + flattenLhsTuple(e, lhsLeaves, stmts); } + // 2) Flatten RHS into expressions (recursively), expanding null to defaults, hoisting side-effects + List rhsLeaves = new ArrayList<>(); + for (ImExpr e : right.getExprs()) { + flattenRhsTuple(e, rhsLeaves, stmts); + } - List tempVars = new ArrayList<>(); - // 2) assign right hand side to temporary variables: - for (ImExpr expr : right.getExprs()) { - ImVar temp = JassIm.ImVar(expr.attrTrace(), expr.attrTyp(), "tuple_temp", false); - expr.setParent(null); - stmts.add(JassIm.ImSet(expr.attrTrace(), JassIm.ImVarAccess(temp), expr)); - tempVars.add(temp); - f.getLocals().add(temp); + // 3) Pad / normalize RHS arity to match LHS arity (needed for nested tuples + null) + for (int i = rhsLeaves.size(); i < lhsLeaves.size(); i++) { + // default for the target component's type + ImType targetT = lhsLeaves.get(i).attrTyp(); + rhsLeaves.add(ImHelper.defaultValueForComplexType(targetT)); + } + + if (rhsLeaves.size() != lhsLeaves.size()) { + throw new RuntimeException("Tuple arity mismatch in set: LHS has " + + lhsLeaves.size() + " leaves, RHS has " + rhsLeaves.size() + + "\nLHS=" + left + "\nRHS=" + right); + } + + // 4) Evaluate RHS leaves first into temps (preserve side-effect order & alias safety) + List temps = new ArrayList<>(rhsLeaves.size()); + for (int i = 0; i < rhsLeaves.size(); i++) { + ImLExpr l = lhsLeaves.get(i); + ImType targetT = l.attrTyp(); + ImExpr r = rhsLeaves.get(i); + + // if a scalar null slipped through, replace with default of target type + if (r instanceof ImNull) { + r = ImHelper.defaultValueForComplexType(targetT); + } + + ImVar t = JassIm.ImVar(r.attrTrace(), targetT, "tuple_temp", false); + f.getLocals().add(t); + + r.setParent(null); + stmts.add(JassIm.ImSet(r.attrTrace(), JassIm.ImVarAccess(t), r)); + temps.add(t); } - // then assign right vars - for (int i = 0; i < leftExprs.size(); i++) { - ImLExpr leftE = (ImLExpr) leftExprs.get(i); - leftE.setParent(null); - stmts.add(JassIm.ImSet(imSet.getTrace(), leftE, JassIm.ImVarAccess(tempVars.get(i)))); + + // 5) Now assign temps into LHS leaves + for (int i = 0; i < lhsLeaves.size(); i++) { + ImLExpr l = lhsLeaves.get(i); + l.setParent(null); + stmts.add(JassIm.ImSet(imSet.getTrace(), l, JassIm.ImVarAccess(temps.get(i)))); } + return ImHelper.statementExprVoid(stmts); } - private static ImStatementExpr inReturn(ImReturn parent, ImTupleExpr tupleExpr, ImTranslator translator, ImFunction f) { - VarsForTupleResult returnVars1 = translator.getTupleTempReturnVarsFor(f); - List returnVars = returnVars1.allValuesStream().collect(Collectors.toList()); + /** Flatten LHS recursively into addressable leaves (ImLExpr), hoisting side-effects */ + private static void flattenLhsTuple(ImExpr e, List out, ImStmts sideStmts) { + ImExpr x = extractSideEffect(e, sideStmts); + if (x instanceof ImTupleExpr) { + for (ImExpr sub : ((ImTupleExpr) x).getExprs()) { + flattenLhsTuple(sub, out, sideStmts); + } + } else { + out.add((ImLExpr) x); + } + } + + /** Flatten RHS recursively into leaves, expanding null to tuple of defaults, hoisting side-effects */ + private static void flattenRhsTuple(ImExpr e, List out, ImStmts sideStmts) { + ImExpr x = extractSideEffect(e, sideStmts); + + // Expand typed nulls for tuple types so arities match + if (x instanceof ImNull) { + ImType t = ((ImNull) x).getType(); + if (t instanceof ImTupleType) { + ImExpr defaults = ImHelper.defaultValueForComplexType(t); // -> ImTupleExpr of defaults + flattenRhsTuple(defaults, out, sideStmts); + return; + } + } + + if (x instanceof ImTupleExpr) { + for (ImExpr sub : ((ImTupleExpr) x).getExprs()) { + flattenRhsTuple(sub, out, sideStmts); + } + } else { + out.add(x); + } + } + + + + private static ImStatementExpr inReturn(ImReturn parent, ImTupleExpr tupleExpr, + ImTranslator translator, ImFunction f) { + // flat list of return temps, already created by translator: + List returnVars = translator.getTupleTempReturnVarsFor(f) + .allValuesStream().collect(Collectors.toList()); + ImStmts stmts = JassIm.ImStmts(); + // 1) Flatten the RHS tuple expression (preserving side effects) + List flatExprs = new ArrayList<>(); + flattenTupleExpr(tupleExpr, stmts, flatExprs); + + // Sanity: + if (flatExprs.size() != returnVars.size()) { + throw new RuntimeException("Tuple arity mismatch in return: RHS has " + + flatExprs.size() + " leaves, but function expects " + + returnVars.size()); + } + + // 2) Assign per component, converting nulls to proper defaults of LHS type for (int i = 0; i < returnVars.size(); i++) { ImVar rv = returnVars.get(i); - ImExpr te = tupleExpr.getExprs().get(i); - te.setParent(null); - stmts.add(JassIm.ImSet(parent.getTrace(), JassIm.ImVarAccess(rv), te)); + ImExpr rhs = flatExprs.get(i); + rhs.setParent(null); + + if (rhs instanceof ImNull) { + // Use the *component target type* to build the correct default (0 for ints, + // (0,0) for tuple components if those ever occur, etc) + ImExpr defaultRhs = ImHelper.defaultValueForComplexType(rv.getType()); + stmts.add(JassIm.ImSet(parent.getTrace(), JassIm.ImVarAccess(rv), defaultRhs)); + } else { + stmts.add(JassIm.ImSet(parent.getTrace(), JassIm.ImVarAccess(rv), rhs)); + } } - stmts.add(JassIm.ImReturn(parent.getTrace(), JassIm.ImVarAccess(returnVars.get(0)))); + // 3) Return the first component temp + stmts.add(JassIm.ImReturn(parent.getTrace(), JassIm.ImVarAccess(returnVars.get(0)))); return ImHelper.statementExprVoid(stmts); } + private static void flattenTupleExpr(ImExpr e, ImStmts intoStmts, List out) { + // Hoist side-effects out of the way first: + ImExpr noSE = extractSideEffect(e, intoStmts); + + // NEW: expand null into a tuple of defaults so arity matches + if (noSE instanceof ImNull) { + ImType t = ((ImNull) noSE).getType(); // already the typed null + if (t instanceof ImTupleType) { + ImExpr defaultTuple = ImHelper.defaultValueForComplexType(t); // -> ImTupleExpr of defaults (recursively) + flattenTupleExpr(defaultTuple, intoStmts, out); + return; + } + } + + if (noSE instanceof ImTupleExpr) { + ImTupleExpr te = (ImTupleExpr) noSE; + for (ImExpr sub : te.getExprs()) { + flattenTupleExpr(sub, intoStmts, out); + } + } else { + out.add(noSE); + } + } + + + private static Element inTupleSelection(ImTupleSelection ts, ImTupleExpr tupleExpr, ImFunction f) { assert ts.getTupleExpr() == tupleExpr; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java index 2617e7db9..c4afd7391 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java @@ -13,8 +13,9 @@ public class ImPrinter { public static void print(ImProg p, Appendable sb, int indent) { for (ImVar g : p.getGlobals()) { g.print(sb, indent); + append(sb,'\n'); } - append(sb, "\n\n"); + append(sb, "\n"); p.getGlobalInits().forEach((v,es) -> { v.print(sb, indent); append(sb, " = "); 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 065a29a60..e3b16ef9c 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 @@ -109,87 +109,141 @@ public static ImStmt translate(StmtForIn forIn, ImTranslator t, ImFunction f) { if (itrType instanceof WurstTypeVararg) { return case_StmtForVararg(forIn, t, f); } - List result = Lists.newArrayList(); + List result = com.google.common.collect.Lists.newArrayList(); Optional iteratorFuncOpt = forIn.attrIteratorFunc(); Optional nextFuncOpt = forIn.attrGetNextFunc(); Optional hasNextFuncOpt = forIn.attrHasNextFunc(); + if (iteratorFuncOpt.isPresent() && nextFuncOpt.isPresent() && hasNextFuncOpt.isPresent()) { FuncLink iteratorFunc = iteratorFuncOpt.get(); FuncLink nextFunc = nextFuncOpt.get(); FuncLink hasNextFunc = hasNextFuncOpt.get(); - // Type of loop Variable: - WurstType loopVarType = forIn.getLoopVar().attrTyp(); + // Type of loop variable (element type S): + WurstType elemType = forIn.getLoopVar().attrTyp(); - // get the iterator function in the intermediate language + // IM functions ImFunction iteratorFuncIm = t.getFuncFor(iteratorFunc.getDef()); - ImFunction nextFuncIm = t.getFuncFor(nextFunc.getDef()); - ImFunction hasNextFuncIm = t.getFuncFor(hasNextFunc.getDef()); + ImFunction nextFuncIm = t.getFuncFor(nextFunc.getDef()); + ImFunction hasNextFuncIm = t.getFuncFor(hasNextFunc.getDef()); - // translate target: + // Translate receiver (iteration target): ImExprs iterationTargetList; - if (forIn.getIn().attrTyp().isStaticRef()) { + if (itrType.isStaticRef()) { iterationTargetList = ImExprs(); } else { - ImExpr iterationTargetIm = forIn.getIn().imTranslateExpr(t, f); + ImExpr iterationTargetIm = iterationTarget.imTranslateExpr(t, f); iterationTargetList = JassIm.ImExprs(iterationTargetIm); } - // call XX.iterator() - ImFunctionCall iteratorCall = ImFunctionCall(forIn, iteratorFuncIm, ImTypeArguments(), iterationTargetList, false, CallType.NORMAL); - // create IM-variable for iterator - ImVar iteratorVar = JassIm.ImVar(forIn.getLoopVar(), iteratorCall.attrTyp(), "iterator", false); + // --- CONCRETE type argument for Iterator and its methods --- + ImType elemImType = elemType.imTranslateType(t); + ImTypeArguments iterTypeArgs = JassIm.ImTypeArguments( + JassIm.ImTypeArgument(elemImType, java.util.Collections.emptyMap()) + ); + + // call XX.iterator()() + ImFunctionCall iteratorCall = ImFunctionCall( + forIn, iteratorFuncIm, iterTypeArgs, iterationTargetList, false, CallType.NORMAL + ); + + // Materialize a concrete IM class type for the iterator local (Iterator) + ImType iteratorImType; + WurstType retWT = iteratorFunc.getReturnType().normalize(); + if (retWT instanceof de.peeeq.wurstscript.types.WurstTypeClass) { + de.peeeq.wurstscript.types.WurstTypeClass rtc = (de.peeeq.wurstscript.types.WurstTypeClass) retWT; + de.peeeq.wurstscript.ast.ClassDef rtClassDef = rtc.getClassDef(); + ImClass imIterClass = t.getClassFor(rtClassDef); + iteratorImType = JassIm.ImClassType(imIterClass, iterTypeArgs.copy()); + } else { + // fallback – should not happen for a well-formed iterator() + iteratorImType = retWT.imTranslateType(t); + } + // locals: iterator and loopVar + ImVar iteratorVar = JassIm.ImVar(forIn.getLoopVar(), iteratorImType, "iterator", false); f.getLocals().add(iteratorVar); f.getLocals().add(t.getVarFor(forIn.getLoopVar())); - // create code for initializing iterator: - ImSet setIterator = ImSet(forIn, ImVarAccess(iteratorVar), iteratorCall); - - result.add(setIterator); + // init iterator + result.add(ImSet(forIn, ImVarAccess(iteratorVar), iteratorCall)); ImStmts imBody = ImStmts(); - // exitwhen not #hasNext() - imBody.add(ImExitwhen(forIn, JassIm.ImOperatorCall(WurstOperator.NOT, JassIm.ImExprs(ImFunctionCall(forIn, hasNextFuncIm, ImTypeArguments(), JassIm.ImExprs - (JassIm - .ImVarAccess(iteratorVar)), false, CallType.NORMAL))))); - // elem = next() - ImFunctionCall nextCall = ImFunctionCall(forIn, nextFuncIm, ImTypeArguments(), JassIm.ImExprs(JassIm.ImVarAccess(iteratorVar)), false, CallType.NORMAL); - WurstType nextReturn = nextFunc.getReturnType(); - ImExpr nextCallWrapped = ExprTranslation.wrapTranslation(forIn, t, nextCall, nextReturn, loopVarType); + + // exitwhen not iterator.hasNext()() + imBody.add(ImExitwhen( + forIn, + JassIm.ImOperatorCall( + de.peeeq.wurstscript.WurstOperator.NOT, + JassIm.ImExprs( + ImFunctionCall( + forIn, hasNextFuncIm, iterTypeArgs.copy(), + JassIm.ImExprs(JassIm.ImVarAccess(iteratorVar)), + false, CallType.NORMAL + ) + ) + ) + )); + + // elem = iterator.next()() + ImFunctionCall nextCall = ImFunctionCall( + forIn, nextFuncIm, iterTypeArgs.copy(), + JassIm.ImExprs(JassIm.ImVarAccess(iteratorVar)), + false, CallType.NORMAL + ); + + ImExpr nextCallWrapped = de.peeeq.wurstscript.translation.imtranslation.ExprTranslation + .wrapTranslation(forIn, t, nextCall, nextFunc.getReturnType(), elemType); imBody.add(ImSet(forIn, ImVarAccess(t.getVarFor(forIn.getLoopVar())), nextCallWrapped)); + // loop body imBody.addAll(t.translateStatements(f, forIn.getBody())); + // optional close()() Optional closeFunc = forIn.attrCloseFunc(); closeFunc.ifPresent(funcLink -> { - - // close iterator before each return imBody.accept(new de.peeeq.wurstscript.jassIm.Element.DefaultVisitor() { @Override public void visit(ImReturn imReturn) { super.visit(imReturn); - imReturn.replaceBy(ImHelper.statementExprVoid(JassIm.ImStmts(ImFunctionCall(forIn, t.getFuncFor(funcLink.getDef()), ImTypeArguments(), JassIm - .ImExprs(JassIm.ImVarAccess(iteratorVar)), false, CallType.NORMAL), imReturn.copy()))); + imReturn.replaceBy( + ImHelper.statementExprVoid( + JassIm.ImStmts( + ImFunctionCall( + forIn, t.getFuncFor(funcLink.getDef()), + iterTypeArgs.copy(), + JassIm.ImExprs(JassIm.ImVarAccess(iteratorVar)), + false, CallType.NORMAL + ), + imReturn.copy() + ) + ) + ); } - }); - }); result.add(ImLoop(forIn, imBody)); - // close iterator after loop - closeFunc.ifPresent(nameLink -> result.add(ImFunctionCall(forIn, t.getFuncFor(nameLink.getDef()), ImTypeArguments(), JassIm.ImExprs(JassIm - .ImVarAccess(iteratorVar)), false, CallType.NORMAL))); + // close after loop + closeFunc.ifPresent(nameLink -> + result.add( + ImFunctionCall( + forIn, t.getFuncFor(nameLink.getDef()), + iterTypeArgs.copy(), + JassIm.ImExprs(JassIm.ImVarAccess(iteratorVar)), + false, CallType.NORMAL + ) + ) + ); } - return ImHelper.statementExprVoid(ImStmts(result)); } + /** * Translate a for in vararg loop. Unlike the other for loops we don't need * an iterator etc. because the loop is unrolled in the VarargEliminator 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 d375534bf..02e8e68dc 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 @@ -776,8 +776,117 @@ private void checkTypeExpr(TypeExpr e) { checkTypeparamsUsedCorrectly(e, tp); } + // Cross-flavor generics ban inside generic declarations: + checkGenericFlavorCompatibility(e); + + } + + // --- Generic flavor detection ---------------------------------------------- + private enum GenericFlavor { NEW, LEGACY } + + /** Returns NEW if all TPs are new style (colon), LEGACY if any exist and none are colon, else null (non-generic). */ + private @Nullable GenericFlavor flavorOf(AstElementWithTypeParameters owner) { + TypeParamDefs tps = owner.getTypeParameters(); + if (tps == null || tps.size() == 0) return null; + + boolean anyNew = false, anyOld = false; + for (TypeParamDef tp : owner.getTypeParameters()) { + if (isTypeParamNewGeneric(tp)) anyNew = true; else anyOld = true; + if (anyNew && anyOld) { + tp.addError("Mixed generic syntax in one declaration is not allowed. Use either or consistently."); + // Pick a flavor to avoid cascaded errors: + return GenericFlavor.NEW; + } + } + if (anyNew) return GenericFlavor.NEW; + if (anyOld) return GenericFlavor.LEGACY; + return null; + } + + /** Returns the flavor of the referenced generic definition, or null if the target is non-generic. */ + private @Nullable GenericFlavor flavorOf(TypeDef def) { + if (def instanceof AstElementWithTypeParameters) { + return flavorOf((AstElementWithTypeParameters) def); + } + return null; + } + + /** Walk up to the nearest *structure* (ClassDef/InterfaceDef) which actually has type parameters. */ + private @Nullable AstElementWithTypeParameters nearestGenericStructureOwner(Element e) { + Element p = e; + while (p != null) { + if (p instanceof ClassDef || p instanceof InterfaceDef) { + AstElementWithTypeParameters a = (AstElementWithTypeParameters) p; + if (a.getTypeParameters() != null && a.getTypeParameters().size() > 0) { + return a; + } + // keep walking if the structure itself has no TPs + } + // IMPORTANT: skip function owners entirely — method generics are allowed to mix. + if (p instanceof FuncDef) { + return null; + } + p = p.getParent(); + } + return null; + } + + /** For type usages inside ANY generic declaration (class/interface/function), + * ban cross-flavor references (NEW cannot use LEGACY and vice versa). */ + private void checkGenericFlavorCompatibility(TypeExpr e) { + // Enforce inside the nearest generic owner: class, interface, or function + AstElementWithTypeParameters owner = nearestGenericOwner(e); + if (owner == null) return; + + @Nullable GenericFlavor ownerFlavor = flavorOf(owner); + if (ownerFlavor == null) return; // owner not actually generic + + // What type is being referenced? + TypeDef targetDef = e.attrTypeDef(); + if (targetDef == null) return; + + // Only care when the referenced definition itself is generic + @Nullable GenericFlavor targetFlavor = flavorOf(targetDef); + if (targetFlavor == null) return; // non-generic target → allowed + + if (ownerFlavor != targetFlavor) { + String targetKind = + (targetDef instanceof ClassDef) ? "class" : + (targetDef instanceof InterfaceDef) ? "interface" : "type"; + String targetName = targetDef.getName(); + + if (ownerFlavor == GenericFlavor.NEW) { + // owner is and target is legacy + e.addError("Cannot reference legacy-generic " + targetKind + " '" + targetName + + "' from a new-generic declaration. Migrate '" + targetName + + "' to '" + targetName + "' or convert this declaration to legacy generics."); + } else { + // owner is legacy and target is new + e.addError("Cannot reference new-generic " + targetKind + " '" + targetName + + "' from a legacy-generic declaration. Use legacy syntax here or migrate this declaration to new generics."); + } + } + } + + + + /** Walk up and find the *nearest* generic declaration owning the current node (class/interface/func). */ + private @Nullable AstElementWithTypeParameters nearestGenericOwner(Element e) { + Element p = e; + while (p != null) { + if (p instanceof FuncDef || p instanceof ClassDef || p instanceof InterfaceDef) { + AstElementWithTypeParameters a = (AstElementWithTypeParameters) p; + if (a.getTypeParameters() != null && a.getTypeParameters().size() > 0) { + return a; + } + } + p = p.getParent(); + } + return null; } + + /** * Checks that module types are only used in valid places */ @@ -812,17 +921,25 @@ private void checkModuleTypeUsedCorrectly(TypeExpr e, ModuleDef md) { * check that type parameters are used in correct contexts: */ private void checkTypeparamsUsedCorrectly(TypeExpr e, TypeParamDef tp) { - if (tp.isStructureDefTypeParam()) { // typeParamDef is for - // structureDef - if (tp.attrNearestStructureDef() instanceof ModuleDef) { - // in modules we can also type-params in static contexts - return; - } + // type param of a structure (class/interface/module)? + if (!tp.isStructureDefTypeParam()) { + return; // free/generic TP outside structures: no special restriction here + } - if (!e.attrIsDynamicContext()) { - e.addError("Type variables must not be used in static contexts."); - } + if (tp.attrNearestStructureDef() instanceof ModuleDef) { + return; } + + if (e.attrIsDynamicContext()) { + return; + } + + if (isTypeParamNewGeneric(tp)) { + return; + } + + // Old-style generics (no colon) or colon bound not reference-like → keep the original restriction + e.addError("Type variables must not be used in static contexts."); } private void checkClosure(ExprClosure e) { @@ -1105,9 +1222,23 @@ private boolean isInConstructor(Element e) { private void checkAssignment(boolean isJassCode, Element pos, WurstType leftType, WurstType rightType) { if (!rightType.isSubtypeOf(leftType, pos)) { + // NEW: Allow null assignment to generic type parameters with colon constraint + if (rightType instanceof WurstTypeNull && leftType instanceof WurstTypeBoundTypeParam) { + WurstTypeBoundTypeParam boundParam = (WurstTypeBoundTypeParam) leftType; + // Allow null for type parameters with colon constraint (class types) + // The translator will handle substituting default values for primitives + return; + } + if (rightType instanceof WurstTypeNull && leftType instanceof WurstTypeTypeParam) { + WurstTypeTypeParam typeParam = (WurstTypeTypeParam) leftType; + // Check if this type parameter has a colon constraint (can be class types) + // Allow the assignment - translator will handle it + return; + } + if (isJassCode) { if (leftType.isSubtypeOf(WurstTypeReal.instance(), pos) - && rightType.isSubtypeOf(WurstTypeInt.instance(), pos)) { + && rightType.isSubtypeOf(WurstTypeInt.instance(), pos)) { // special case: jass allows to assign an integer to a real // variable return; @@ -1645,6 +1776,14 @@ private void checkReturnInFunc(StmtReturn s, FunctionImplementation func) { } else { WurstType returnedType = returned.attrTyp(); if (!returnedType.isSubtypeOf(returnType, s)) { + // NEW: Allow returning null for generic type parameters + if (returnedType instanceof WurstTypeNull && + (returnType instanceof WurstTypeBoundTypeParam || returnType instanceof WurstTypeTypeParam)) { + // Allow null return for generic type parameters + // The translator will handle substituting default values for primitives + return; + } + s.addError("Cannot return " + returnedType + ", expected expression of type " + returnType); } } @@ -1821,7 +1960,7 @@ public VariableBinding case_ExprMemberMethodDotDot(ExprMemberMethodDotDot e) { WurstType typ = boundTyp.getBaseType(); TypeParamDef tp = t._1(); - if (tp.getTypeParamConstraints() instanceof TypeExprList) { + if (isTypeParamNewGeneric(tp)) { // new style generics } else { // old style generics @@ -1889,6 +2028,10 @@ public VariableBinding case_ExprMemberMethodDotDot(ExprMemberMethodDotDot e) { } } + private static boolean isTypeParamNewGeneric(TypeParamDef tp) { + return tp.getTypeParamConstraints() instanceof TypeExprList; + } + private void checkFuncRef(FuncRef ref) { if (ref.getFuncName().isEmpty()) { ref.addError("Missing function name."); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/controlflow/DataflowAnomalyAnalysis.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/controlflow/DataflowAnomalyAnalysis.java index 828c01e94..9747becd7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/controlflow/DataflowAnomalyAnalysis.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/controlflow/DataflowAnomalyAnalysis.java @@ -505,7 +505,13 @@ void checkFinal(VarStates fin) { if (ur instanceof StmtSet) { errorPos = ((StmtSet) ur).getUpdatedExpr(); } - errorPos.addWarning("The assignment to " + Utils.printElement(var) + " is never read."); + @Nullable ExprClosure exprClosure = errorPos.attrNearestExprClosure(); + @Nullable ExprClosure exprClosure1 = var.attrNearestExprClosure(); + if (exprClosure != null && exprClosure != exprClosure1) { + errorPos.addWarning("This assignment to the closure-captured variable " + Utils.printElement(var) + " has no effect outside the closure."); + } else { + errorPos.addWarning("The assignment to " + Utils.printElement(var) + " is never read."); + } } } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java index 73a122a5d..e60d417fe 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java @@ -827,6 +827,26 @@ public void unreadVarWarning2() { // #380 ); } + @Test + public void unreadVarWarning3() { // #380 + testAssertErrorsLines(true, "closure-captured variable", + "package test", + "@annotation public function annotation()", + "@annotation public function extern()", + "@extern native I2S(int x) returns string", + "native testSuccess()", + "interface Fn", + " function apply()", + "function foo(Fn _f)", + "init", + " var i = 5", + " foo() ->", + " i++", + " if i == 5", + " testSuccess()" + ); + } + @Test public void unreadVarWarningArrays() { // #813 diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java index da56c8e39..58879e31d 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/GenericsWithTypeclassesTests.java @@ -194,15 +194,16 @@ public void function() { " return true", " function next() returns S", " return t", + " function close()", + " skip", " class A", " class B", " class C", " init", " List a = new List()", -// " for B b in a", - " Iterator iterator = a.iterator()", - " while iterator.hasNext()", - " B b = iterator.next()", + " for B b in a", +// " Iterator iterator = a.iterator()", +// " while iterator.hasNext()", " testSuccess()", "endpackage" ); @@ -226,7 +227,7 @@ public void testSubtypeGenericClass2() { "package test", " class A", " class B extends A", - " function foo()", + " function foo()", " A x = new B", "endpackage" ); @@ -670,7 +671,7 @@ public void genericExtensionMethod1() { @Test public void genericReturnOverride() { - testAssertErrorsLines(false, "Cannot return null, expected expression of type T", + testAssertOkLines(false, "package Test", "interface I", " function f() returns T", @@ -1129,7 +1130,7 @@ public void nullWithGeneric() { @Test public void missingTypeArgsFunc() { - testAssertErrorsLines(false, "Cannot return null, expected expression of type T", + testAssertErrorsLines(false, "Cannot infer type for type parameter T, T", "package test", "function foo() returns T", " return null", @@ -1140,7 +1141,7 @@ public void missingTypeArgsFunc() { @Test public void missingTypeArgsMethod() { - testAssertErrorsLines(false, "Cannot return null, expected expression of type T", + testAssertErrorsLines(false, "Cannot infer type for type parameter T, T", "package test", "class C", " function foo() returns T", @@ -1164,7 +1165,7 @@ public void missingTypeArgsConstructor() { @Test public void tooManyTypeArgsFunc() { - testAssertErrorsLines(false, "Cannot return null, expected expression of type T", + testAssertErrorsLines(false, "Too many type arguments given", "package test", "function foo() returns T", " return null", @@ -1175,7 +1176,7 @@ public void tooManyTypeArgsFunc() { @Test public void tooManyTypeArgsMethod() { - testAssertErrorsLines(false, "Cannot return null, expected expression of type T", + testAssertErrorsLines(false, "Too many type arguments given", "package test", "class C", " function foo() returns T", @@ -1274,4 +1275,261 @@ public void abstractReturnT() { ); } + @Test + public void genericVar_instanceMethods_runtime() { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " class Box", + " private T store", + " function put(T v)", + " store = v // instance method writes static generic array", + " function get() returns T", + " return store // instance method reads static generic array", + " init", + " let bi = new Box", + " let br = new Box", + " bi.put(42)", + " br.put(1.5)", + " let xi = bi.get()", + " let xr = br.get()", + " if xi == 42 and xr == 1.5", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void genericStaticVar_instanceMethods_runtime() { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " class Box", + " private static T store", + " function put(T v)", + " store = v // instance method writes static generic array", + " function get() returns T", + " let i = 1", + " if i < 1", + " return null", + " return store // instance method reads static generic array", + " function clear()", + " store = null", + " init", + " let bi = new Box", + " let br = new Box", + " bi.put(42)", + " br.put(1.5)", + " let xi = bi.get()", + " let xr = br.get()", + " bi.clear()", + " br.clear()", + " let xi2 = bi.get()", + " let xr2 = br.get()", + " if xi == 42 and xr == 1.5 and xi2 == 0 and 1.0001 - xr2 > 1.", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void genericStaticVar_Class() { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " class A", + " class B", + " class Box", + " private static T store", + " function put(T v)", + " store = v // instance method writes static generic array", + " function get() returns T", + " let i = 1", + " if i < 1", + " return null", + " return store // instance method reads static generic array", + " function clear()", + " store = null", + " init", + " let bi = new Box", + " let br = new Box", + " bi.put(new A())", + " br.put(new B())", + " let xi = bi.get()", + " let xr = br.get()", + " bi.clear()", + " br.clear()", + " let xi2 = bi.get()", + " let xr2 = br.get()", + " if xi != null and xr != null and xi2 == null and xr2 == null", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void genericStaticArray_instanceMethods_runtime() { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " class Box", + " private static T array store", + " function put(int i, T v)", + " store[i] = v // instance method writes static generic array", + " function get(int i) returns T", + " let i2 = 1", + " if i2 < 1", + " return null", + " return store[i] // instance method reads static generic array", + " function clear(int i)", + " store[i] = null", + " init", + " let bi = new Box", + " let br = new Box", + " bi.put(3, 42)", + " br.put(1, 1.5)", + " let xi = bi.get(3)", + " let xr = br.get(1)", + " bi.clear(3)", + " br.clear(1)", + " let xi2 = bi.get(3)", + " let xr2 = br.get(1)", + " if xi == 42 and xr == 1.5 and xi2 == 0 and 1.0001 - xr2 > 1.", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void genericStaticTuple_runtime() { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " tuple ntup(int i, int i2)", + " tuple tup(int i, ntup nt)", + " class BoxItr", + " private int i = 0", + " private Box box", + " construct(Box box)", + " this.box = box", + " function next() returns T", + " return null", + " class Box", + " private static T array store", + " function iterator() returns BoxItr", + " return new BoxItr(this)", + " function put(int i, T v)", + " store[i] = v // instance method writes static generic array", + " function get(int i) returns T", + " let i2 = 1", + " if i2 < 1", + " return null", + " return store[i] // instance method reads static generic array", + " function clear(int i)", + " store[i] = null", + " init", + " let bi = new Box", + " let itr = bi.iterator()", + " bi.put(1, tup(3, ntup(42, 17)))", + " let xi = bi.get(1).i", + " let xr = bi.get(1).nt.i", + " bi.clear(1)", + " let xr2 = bi.get(1).i", + " if xi == 3 and xr == 42 and xr2 == 0 and itr.next().i == 0", + " testSuccess()", + "endpackage" + ); + } + + @Test + public void mixingNewOwner_legacyType_classField() { + testAssertErrorsLines(false, + "Cannot reference legacy-generic", + "package test", + "class B", + "class A", + " B b" + ); + } + + @Test + public void mixingLegacyOwner_newType_classField() { + testAssertErrorsLines(false, + "Cannot reference new-generic", + "package test", + "class B", + "class A", + " B b" + ); + } + + @Test + public void mixingNewOwner_legacyType_functionReturn() { + testAssertErrorsLines(false, + "Cannot reference legacy-generic", + "package test", + "class B", + "function makeB() returns B", + " return null" + ); + } + + @Test + public void mixingLegacyOwner_newType_functionReturn() { + testAssertErrorsLines(false, + "Cannot reference new-generic", + "package test", + "class B", + "function makeB() returns B", + " return null" + ); + } + + @Test + public void mixingNewOwner_legacyType_inExtendsClause() { + testAssertErrorsLines(false, + "Cannot reference legacy-generic", + "package test", + "interface I", + "class C implements I" + ); + } + + @Test + public void mixingLegacyOwner_newType_methodReturn() { + testAssertErrorsLines(false, + "Cannot reference new-generic", + "package test", + "interface I", + "class C", + " function f() returns I", + " return null" + ); + } + + @Test + public void mixingNewOwner_legacyType_nestedGenericUse() { + testAssertErrorsLines(false, + "Cannot reference legacy-generic", + "package test", + "class Box", + "class B", + "class A", + " Box> field" + ); + } + + @Test + public void mixingLegacyOwner_newType_insideGenericClassMethod() { + testAssertErrorsLines(false, + "Cannot reference new-generic", + "package test", + "class B", + "class A", + " function usee()", + " B x = null" + ); + } + + } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java index 107f4f25e..2647f59b6 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java @@ -483,6 +483,7 @@ private void translateAndTest(String name, boolean executeProg, executeTests(gui, compiler.getImTranslator(), imProg); } if (executeProg) { + WLogger.info("Executing imProg before jass transformation"); executeImProg(gui, imProg); } } @@ -498,6 +499,7 @@ private void translateAndTest(String name, boolean executeProg, executeTests(gui, compiler.getImTranslator(), imProg); } if (executeProg) { + WLogger.info("Executing imProg after jass transformation"); executeImProg(gui, imProg); } @@ -568,6 +570,7 @@ private void executeImProg(WurstGui gui, ImProg imProg) throws TestFailException interpreter.addNativeProvider(new ReflectionNativeProvider(interpreter)); interpreter.executeFunction("main", null); } catch (TestSuccessException e) { + System.out.println("Suceed function called!"); return; } throw new Error("Succeed function not called");