diff --git a/ClangAstParser/build.gradle b/ClangAstParser/build.gradle index 26a50196f..81313b2d7 100644 --- a/ClangAstParser/build.gradle +++ b/ClangAstParser/build.gradle @@ -22,6 +22,7 @@ dependencies { implementation ":jOptions" implementation ":SpecsUtils" + implementation 'com.google.code.gson:gson:2.12.1' implementation 'com.google.guava:guava:33.4.0-jre' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' @@ -49,6 +50,10 @@ sourceSets { } } +processResources { + from('clang-dumper-release.tag') +} + // Test coverage configuration jacocoTestReport { reports { diff --git a/ClangAstParser/clang-dumper-release.tag b/ClangAstParser/clang-dumper-release.tag new file mode 100644 index 000000000..9acbd12ca --- /dev/null +++ b/ClangAstParser/clang-dumper-release.tag @@ -0,0 +1 @@ +v18.1.8_1 diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstFileResource.java b/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstFileResource.java deleted file mode 100644 index 6e2ac72f6..000000000 --- a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstFileResource.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright 2018 SPeCS. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package pt.up.fe.specs.clang; - -import pt.up.fe.specs.util.providers.FileResourceProvider; - -import java.util.function.Supplier; - -public enum ClangAstFileResource implements Supplier { - - LIBC_CXX_LINUX_COMPLETE(ClangAstWebResource.LIBC_CXX_LINUX_COMPLETE), - LIBC_CXX_MACOS_COMPLETE(ClangAstWebResource.LIBC_CXX_MACOS_COMPLETE), - LIBC_CXX_WIN32_COMPLETE(ClangAstWebResource.LIBC_CXX_WIN32_COMPLETE), - OPENMP_INCLUDES(ClangAstWebResource.OPENMP_INCLUDES), - CUDA_LIB(ClangAstWebResource.CUDA_LIB), - WIN_EXE(ClangAstWebResource.WIN_EXE), - WIN_DLL1(ClangAstWebResource.WIN_DLL1), - WIN_DLL2(ClangAstWebResource.WIN_DLL2), - WIN_DLL3(ClangAstWebResource.WIN_DLL3), - WIN_DLL4(ClangAstWebResource.WIN_DLL4), - WIN_DLL5(ClangAstWebResource.WIN_DLL5), - WIN_DLL6(ClangAstWebResource.WIN_DLL6), - WIN_DLL7(ClangAstWebResource.WIN_DLL7), - WIN_DLL8(ClangAstWebResource.WIN_DLL8), - WIN_DLL9(ClangAstWebResource.WIN_DLL9), - WIN_CLANG_DLL(ClangAstWebResource.WIN_CLANG_DLL), - WIN_LLVM_DLL(ClangAstWebResource.WIN_LLVM_DLL), - LINUX_EXE(ClangAstWebResource.LINUX_EXE), - LINUX_PLUGIN(ClangAstWebResource.LINUX_PLUGIN), - LINUX_LLVM_DLL(ClangAstWebResource.LINUX_LLVM_DLL), - MAC_OS_EXE(ClangAstWebResource.MAC_OS_EXE), - MAC_OS_LLVM_DLL(ClangAstWebResource.MAC_OS_LLVM_DLL), - MAC_OS_DLL1(ClangAstWebResource.MAC_OS_DLL1); - - private final FileResourceProvider provider; - - ClangAstFileResource(FileResourceProvider provider) { - this.provider = provider; - } - - @Override - public FileResourceProvider get() { - return provider; - } -} diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstKeys.java b/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstKeys.java index fd9907689..1fef8afdf 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstKeys.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstKeys.java @@ -16,6 +16,7 @@ import org.suikasoft.jOptions.Datakey.DataKey; import org.suikasoft.jOptions.Datakey.KeyFactory; import org.suikasoft.jOptions.Interfaces.DataStore; +import pt.up.fe.specs.clang.ClangAstWebResource.LocalBuild; import pt.up.fe.specs.clava.ClavaLog; import pt.up.fe.specs.clava.ClavaOptions; import pt.up.fe.specs.clava.language.Standard; @@ -27,8 +28,6 @@ public interface ClangAstKeys { - DataKey CLANGAST_VERSION = KeyFactory.string("clangast_version", ""); - /** * What libc/libcxx mode should be used. */ @@ -55,7 +54,6 @@ public static String getFlagIgnoreIncludes() { static DataStore toDataStore(List flags) { DataStore config = DataStore.newInstance(ClavaOptions.STORE_DEFINITION, false); final String stdPrefix = "-std="; - final String clangAstDumperPrefix = "-clang-dumper="; final String cilkFlag = "-fcilkplus"; // Search options @@ -78,13 +76,6 @@ static DataStore toDataStore(List flags) { continue; } - // If ClangAstDumper version, parse option - if (flag.startsWith(clangAstDumperPrefix)) { - String version = flag.substring(clangAstDumperPrefix.length()); - config.set(ClangAstKeys.CLANGAST_VERSION, version); - continue; - } - // If Cilk flag, add option if (flag.equals(cilkFlag)) { config.set(ClangAstKeys.USES_CILK); @@ -108,6 +99,10 @@ static DataStore toDataStore(List flags) { config.add(ClavaOptions.FLAGS_LIST, parsedFlags); + if (ClangAstWebResource.getDumperSource() instanceof LocalBuild) { + config.set(ClangAstKeys.LIBC_CXX_MODE, LibcMode.SYSTEM); + } + return config; } diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstResource.java b/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstResource.java index a80188a10..c0c53d5ca 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstResource.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstResource.java @@ -20,12 +20,9 @@ * */ public enum ClangAstResource implements ResourceProvider { - // BUILTIN_INCLUDES_3_8(ClangAstWebResource.BUILTIN_INCLUDES_3_8), - TEST_INCLUDES_C("test_includes.c"), TEST_INCLUDES_CPP("test_includes.cpp"); - // private final WebResourceProvider webResource; private final String resource; private static final String basePackage = "clangast/"; @@ -35,24 +32,10 @@ public enum ClangAstResource implements ResourceProvider { */ private ClangAstResource(String resource) { this.resource = basePackage + resource; - // this.webResource = null; } - // private ClangAstResource(WebResourceProvider webResource) { - // this.resource = null; - // this.webResource = webResource; - // } - - /* (non-Javadoc) - * @see org.suikasoft.SharedLibrary.Interfaces.ResourceProvider#getResource() - */ @Override public String getResource() { return resource; - // if (resource != null) { - // return resource; - // } - - // return webResource.getResourceUrl(); } } diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstWebResource.java b/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstWebResource.java index 3a779583d..9689587a9 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstWebResource.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/ClangAstWebResource.java @@ -13,40 +13,148 @@ package pt.up.fe.specs.clang; +import com.google.gson.Gson; +import pt.up.fe.specs.util.SpecsIo; import pt.up.fe.specs.util.providers.WebResourceProvider; -public interface ClangAstWebResource { +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; - String ROOT_16_0_5 = "https://github.com/specs-feup/clava/releases/download/clang_ast_dumper_16.0.5/"; - String ROOT_12_0_7 = "https://github.com/specs-feup/clava/releases/download/clang_ast_dumper_v12.0.7.1/"; +public final class ClangAstWebResource { - WebResourceProvider LIBC_CXX_LINUX_COMPLETE = WebResourceProvider.newInstance(ROOT_16_0_5, "libc_cxx_linux_complete.zip", "v16.0.5"); - WebResourceProvider LIBC_CXX_MACOS_COMPLETE = WebResourceProvider.newInstance(ROOT_16_0_5, "libc_cxx_macos_complete.zip", "v16.0.6"); - WebResourceProvider LIBC_CXX_WIN32_COMPLETE = WebResourceProvider.newInstance(ROOT_16_0_5, "libc_cxx_win32_complete.zip", "v16.0.5"); + private static final String RELEASE_ROOT = "https://github.com/specs-feup/clang-dumper/releases/download/"; + private static final String CUDA_RELEASE_ROOT = + "https://github.com/specs-feup/clava/releases/download/clang_ast_dumper_v12.0.7.1/"; + public static final String CUDA_LIB_FILENAME = "cudalib.zip"; + static final WebResourceProvider CUDA_LIB = + WebResourceProvider.newInstance(CUDA_RELEASE_ROOT, CUDA_LIB_FILENAME, "v11.3.0"); + private static final String RELEASE_TAG_RESOURCE = "clang-dumper-release.tag"; + public static final String MANIFEST_FILENAME = "clang-dumper-release-manifest.json"; - WebResourceProvider OPENMP_INCLUDES = WebResourceProvider.newInstance(ROOT_16_0_5, "openmp_includes.zip"); + private static final Gson GSON = new Gson(); + private static final DumperSource DUMPER_SOURCE = readDumperSource(); - WebResourceProvider CUDA_LIB = WebResourceProvider.newInstance(ROOT_12_0_7, "cudalib.zip", "v11.3.0"); + private ClangAstWebResource() { + } - WebResourceProvider WIN_EXE = WebResourceProvider.newInstance(ROOT_16_0_5, "clang_ast_windows.exe", "v16.0.5_1"); - WebResourceProvider WIN_DLL1 = WebResourceProvider.newInstance(ROOT_12_0_7, "libwinpthread-1.dll"); - WebResourceProvider WIN_DLL2 = WebResourceProvider.newInstance(ROOT_12_0_7, "zlib1.dll"); - WebResourceProvider WIN_DLL3 = WebResourceProvider.newInstance(ROOT_12_0_7, "libzstd.dll"); - WebResourceProvider WIN_DLL4 = WebResourceProvider.newInstance(ROOT_12_0_7, "libstdc++-6.dll"); - WebResourceProvider WIN_DLL5 = WebResourceProvider.newInstance(ROOT_12_0_7, "libgcc_s_seh-1.dll"); - WebResourceProvider WIN_DLL6 = WebResourceProvider.newInstance(ROOT_12_0_7, "libffi-8.dll"); - WebResourceProvider WIN_DLL7 = WebResourceProvider.newInstance(ROOT_12_0_7, "libxml2-2.dll"); - WebResourceProvider WIN_DLL8 = WebResourceProvider.newInstance(ROOT_12_0_7, "liblzma-5.dll"); - WebResourceProvider WIN_DLL9 = WebResourceProvider.newInstance(ROOT_12_0_7, "libiconv-2.dll"); - WebResourceProvider WIN_LLVM_DLL = WebResourceProvider.newInstance(ROOT_16_0_5, "libLLVM-16.dll"); - WebResourceProvider WIN_CLANG_DLL = WebResourceProvider.newInstance(ROOT_16_0_5, "libclang-cpp.dll"); + public static DumperSource getDumperSource() { + return DUMPER_SOURCE; + } - WebResourceProvider LINUX_EXE = WebResourceProvider.newInstance(ROOT_16_0_5, "clang_ast_linux", "v16.0.5"); - WebResourceProvider LINUX_PLUGIN = WebResourceProvider.newInstance(ROOT_16_0_5, "clang-plugin.so", "v16.0.5"); - WebResourceProvider LINUX_LLVM_DLL = WebResourceProvider.newInstance(ROOT_16_0_5, "libLLVM-16.so.1", "v16.0.5"); + private static DumperSource readDumperSource() { + var inputStream = ClangAstWebResource.class.getClassLoader().getResourceAsStream(RELEASE_TAG_RESOURCE); - WebResourceProvider MAC_OS_EXE = WebResourceProvider.newInstance(ROOT_16_0_5, "clang_ast_macos", "v16.0.5"); - WebResourceProvider MAC_OS_LLVM_DLL = WebResourceProvider.newInstance(ROOT_16_0_5, "libLLVM.dylib", "v16.0.5"); - WebResourceProvider MAC_OS_DLL1 = WebResourceProvider.newInstance(ROOT_16_0_5, "libzstd.1.dylib", "v16.0.5"); + if (inputStream == null) { + throw new RuntimeException("Could not find resource '" + RELEASE_TAG_RESOURCE + "'"); + } + String value; + try (inputStream) { + value = SpecsIo.read(inputStream).trim(); + } catch (IOException e) { + throw new UncheckedIOException("Could not read resource '" + RELEASE_TAG_RESOURCE + "'", e); + } + + if (value.isBlank()) { + throw new RuntimeException("Resource '" + RELEASE_TAG_RESOURCE + "' is empty"); + } + + return parseDumperSource(value); + } + + static DumperSource parseDumperSource(String value) { + var path = Path.of(value); + if (path.isAbsolute()) { + return new LocalBuild(path.toFile()); + } + + if (value.contains("/") || value.contains("\\") || value.equals(".") || value.equals("..")) { + throw new RuntimeException("Relative paths are not supported in resource '" + RELEASE_TAG_RESOURCE + + "': '" + value + "'"); + } + + return new Release(value); + } + + public static String getReleaseTag() { + var source = getDumperSource(); + if (source instanceof Release release) { + return release.tag(); + } + + throw new IllegalStateException("The clang-dumper resource points to a local build"); + } + + public static ClangDumperManifest getManifest(File resourceFolder) { + var releaseTag = getReleaseTag(); + var manifestResource = WebResourceProvider.newInstance(getReleaseBaseUrl(releaseTag), MANIFEST_FILENAME, + releaseTag); + var manifestFile = manifestResource.writeVersioned(resourceFolder, ClangAstWebResource.class).getFile(); + var manifest = GSON.fromJson(SpecsIo.read(manifestFile), ClangDumperManifest.class); + + if (manifest == null) { + throw new RuntimeException("Could not parse clang-dumper manifest from '" + manifestFile + "'"); + } + + manifest.validate(); + return manifest; + } + + public static WebResourceProvider getAssetResource(ClangDumperManifestAsset asset) { + var releaseTag = getReleaseTag(); + return WebResourceProvider.newInstance(getReleaseBaseUrl(releaseTag), asset.filename(), + releaseTag + "-" + asset.sha256()); + } + + private static String getReleaseBaseUrl(String releaseTag) { + return RELEASE_ROOT + releaseTag + "/"; + } + + public sealed interface DumperSource permits Release, LocalBuild { + } + + public record Release(String tag) implements DumperSource { + } + + public record LocalBuild(File folder) implements DumperSource { + } + + public record ClangDumperManifest(int schema_version, List assets) { + + public void validate() { + if (schema_version != 1) { + throw new RuntimeException("Unsupported clang-dumper manifest schema version: " + schema_version); + } + + if (assets == null || assets.isEmpty()) { + throw new RuntimeException("Clang-dumper manifest does not contain assets"); + } + } + + public ClangDumperManifestAsset getAsset(String platform, String arch, String kind) { + Objects.requireNonNull(platform); + Objects.requireNonNull(arch); + Objects.requireNonNull(kind); + + Optional asset = assets.stream() + .filter(candidate -> candidate.matches(platform, arch, kind)) + .findFirst(); + + return asset.orElseThrow(() -> new RuntimeException("Could not find clang-dumper asset for platform '" + + platform + "', architecture '" + arch + "' and kind '" + kind + "'")); + } + } + + public record ClangDumperManifestAsset(String filename, String kind, String platform, String arch, int llvm_major, + String sha256) { + + public boolean matches(String platform, String arch, String kind) { + return this.platform.equals(platform) && this.arch.equals(arch) && this.kind.equals(kind); + } + } } diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/ClangResources.java b/ClangAstParser/src/pt/up/fe/specs/clang/ClangResources.java index 3aa4b5659..9d54bf5eb 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/ClangResources.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/ClangResources.java @@ -13,6 +13,9 @@ package pt.up.fe.specs.clang; +import pt.up.fe.specs.clang.ClangAstWebResource.ClangDumperManifest; +import pt.up.fe.specs.clang.ClangAstWebResource.ClangDumperManifestAsset; +import pt.up.fe.specs.clang.ClangAstWebResource.LocalBuild; import pt.up.fe.specs.clang.codeparser.CodeParser; import pt.up.fe.specs.clang.dumper.ClangAstDumper; import pt.up.fe.specs.clang.parsers.TopLevelNodesParser; @@ -20,111 +23,152 @@ import pt.up.fe.specs.util.SpecsIo; import pt.up.fe.specs.util.SpecsLogs; import pt.up.fe.specs.util.SpecsSystem; -import pt.up.fe.specs.util.lazy.Lazy; -import pt.up.fe.specs.util.providers.FileResourceManager; -import pt.up.fe.specs.util.providers.FileResourceProvider; import pt.up.fe.specs.util.providers.FileResourceProvider.ResourceWriteData; import pt.up.fe.specs.util.system.ProcessOutputAsString; import java.io.File; -import java.util.*; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; public class ClangResources { - private static final FileResourceManager CLANG_AST_RESOURCES = FileResourceManager - .fromEnum(ClangAstFileResource.class); - private static final Map CLANG_FILES_CACHE = new ConcurrentHashMap<>(); - + private static final Map CLANG_FILES_LOCKS = new ConcurrentHashMap<>(); private final static String CLANG_FOLDERNAME = "clang_ast_exe"; + private final static String INCLUDES_FOLDERNAME = "includes"; + private final static String LAST_USED_FILENAME = "last-used.txt"; + private final static String CACHE_LOCK_FOLDERNAME = ".cache.lock"; + private final static String CACHE_LOCK_OWNER_PREFIX = "owner-"; + private final static Duration CACHE_LOCK_RETRY_INTERVAL = Duration.ofMillis(100); + private final static Duration CACHE_LOCK_STALE_MAX_AGE = Duration.ofMinutes(5); + private final static Duration STALE_CACHE_MAX_AGE = Duration.ofDays(60); - private final Lazy cudalibFolder = Lazy.newInstance(this::prepareBuiltinCudaLib); + private static final Map HAS_LIBC = new ConcurrentHashMap<>(); private final CodeParser options; - - private static final AtomicInteger HAS_LIBC = new AtomicInteger(-1); - public ClangResources(CodeParser options) { this.options = options; } - public ClangFiles getClangFiles(String version, LibcMode libcMode) { - - // Create key - var key = libcMode.name() + "_" + version + "_" + getClangResourceFolder().getAbsolutePath(); + public File getBuiltinCudaLib() { + var cudaResourceFolder = SpecsIo.mkdir(options.get(CodeParser.DUMPER_FOLDER), "cuda"); + var cudaFolder = SpecsIo.mkdir(cudaResourceFolder, "cudalib"); + var zipFile = ClangAstWebResource.CUDA_LIB.writeVersioned(cudaResourceFolder, ClangResources.class); - // Check if cached - var files = CLANG_FILES_CACHE.get(key); - if (files != null) { - SpecsLogs.debug(() -> "Using cached version of Clang files: " + files); - return files; + if (zipFile.isNewFile() || !isCudaInstallation(cudaFolder)) { + SpecsIo.deleteFolderContents(cudaFolder); + SpecsIo.extractZip(zipFile.getFile(), cudaFolder); } - File clangExecutable = prepareResources(version); - List builtinIncludes = prepareIncludes(clangExecutable, libcMode); + return cudaFolder; + } - var newFiles = new ClangFiles(clangExecutable, builtinIncludes); - SpecsLogs.debug(() -> "Using downloaded version of Clang files: " + newFiles); + private static boolean isCudaInstallation(File folder) { + return folder.isDirectory() && new File(folder, "include/cuda_runtime.h").isFile(); + } - // Store in cache - CLANG_FILES_CACHE.put(key, newFiles); + public ClangFiles getClangFiles(LibcMode libcMode) { - return newFiles; - } + var source = ClangAstWebResource.getDumperSource(); - /** - * @return path to the executable that was copied - */ - private File prepareResources(String version) { + if (source instanceof LocalBuild localBuild) { + return new ClangFiles(getLocalExecutable(localBuild.folder()), List.of()); + } - File resourceFolder = getClangResourceFolder(); + var useBuiltinCuda = options.get(CodeParser.CUDA_PATH).equalsIgnoreCase(CodeParser.getBuiltinOption()); + var resourceFolder = getClangResourceFolder(); + var key = libcMode.name() + "_" + useBuiltinCuda + "_" + source + "_" + + resourceFolder.getAbsolutePath(); + var jvmLock = CLANG_FILES_LOCKS.computeIfAbsent(resourceFolder.getAbsolutePath(), ignored -> new Object()); + synchronized (jvmLock) { + var lockFolder = getCacheLockFolder(resourceFolder); + try (var ignored = acquireCacheLock(resourceFolder)) { + var files = CLANG_FILES_CACHE.get(key); + if (files != null) { + if (files.clangExecutable().isFile()) { + writeLastUsed(resourceFolder, Instant.now()); + SpecsLogs.debug(() -> "Using cached version of Clang files: " + files); + return files; + } + + CLANG_FILES_CACHE.remove(key, files); + } - SupportedPlatform platform = SupportedPlatform.getCurrentPlatform(); - FileResourceProvider executableResource = getVersionedResource(getExecutableResource(platform), version); + var manifest = ClangAstWebResource.getManifest(resourceFolder); + File clangExecutable = prepareResources(manifest, resourceFolder); + List builtinIncludes = prepareIncludes(manifest, resourceFolder, clangExecutable, libcMode); - // Copy executable - ResourceWriteData executable = executableResource.writeVersioned(resourceFolder, ClangResources.class); + if (useBuiltinCuda) { + getBuiltinCudaLib(); + } - // If Windows, copy additional dependencies - if (platform == SupportedPlatform.WINDOWS) { - for (FileResourceProvider resource : getWindowsResources()) { - resource.writeVersioned(resourceFolder, ClangResources.class); - } - } else if (platform == SupportedPlatform.MAC_OS) { - for (FileResourceProvider resource : getMacOSResources()) { - resource.writeVersioned(resourceFolder, ClangResources.class); - } - } else if (platform == SupportedPlatform.LINUX) { - for (FileResourceProvider resource : getLinuxResources()) { - resource.writeVersioned(resourceFolder, ClangResources.class); + validateTopLevelCacheFiles(manifest, resourceFolder); + updateLastUsedAndCleanupStaleVersions(resourceFolder); + + var newFiles = new ClangFiles(clangExecutable, builtinIncludes); + SpecsLogs.debug(() -> "Using downloaded version of Clang files: " + newFiles); + + CLANG_FILES_CACHE.put(key, newFiles); + return newFiles; + } catch (IOException e) { + throw new UncheckedIOException("Could not lock clang-dumper cache '" + lockFolder + "'", e); } } + } + + static File getLocalExecutable(File buildFolder) { + if (!buildFolder.isDirectory()) { + throw new RuntimeException("Local clang-dumper build directory does not exist: '" + buildFolder + "'"); + } + + String filename; + if (ClangAstDumper.usePlugin()) { + filename = System.mapLibraryName("plugin"); + } else { + filename = SupportedPlatform.getCurrentPlatform().isWindows() ? "tool.exe" : "tool"; + } + + var executable = new File(buildFolder, filename); + if (!executable.isFile()) { + throw new RuntimeException("Could not find local clang-dumper " + + (ClangAstDumper.usePlugin() ? "plugin" : "tool") + " '" + executable + "'"); + } + + SpecsLogs.info("Using local clang-dumper build: " + executable); + return executable; + } + + private File prepareResources(ClangDumperManifest manifest, File resourceFolder) { + SupportedPlatform platform = SupportedPlatform.getCurrentPlatform(); + + var executableKind = ClangAstDumper.usePlugin() ? "plugin" : "tool"; + ResourceWriteData executable = downloadAsset(manifest, executableKind, resourceFolder); - // If on Windows, preemptively unblock file, due to possible Mark-of-the-Web restrictions if (platform.isWindows()) { - var command = List.of(SpecsSystem.getWindowsPowershell(), "-NoLogo", "-NoProfile", "-NonInteractive", - "-ExecutionPolicy", "Bypass", - "-Command", - "Unblock-File", - "-Path", - "\"" + executable.getFile().getAbsolutePath() + "\"", - "-ErrorAction", - "Stop" - ); - - var output = SpecsSystem.runProcess(command, true, true); - if (output.getReturnValue() == 0) { - SpecsLogs.info("Successfully unblocked dumper executable"); - } else { - SpecsLogs.info("Could not unblock dumper executable"); - } + unblockWindowsFile(executable.getFile()); } - // If file is new and we are in a flavor of Linux or MacOS, make file executable if (executable.isNewFile() && (platform.isLinux() || platform.isMacOs())) { SpecsSystem.runProcess(Arrays.asList("chmod", "+x", executable.getFile().getAbsolutePath()), false, true); } @@ -132,270 +176,521 @@ private File prepareResources(String version) { return executable.getFile(); } - private FileResourceProvider getVersionedResource(FileResourceProvider resource, String version) { - - // If version not defined, use the latest version of the resource - if (version.isEmpty()) { - version = resource.version(); + private void unblockWindowsFile(File executable) { + var command = List.of(SpecsSystem.getWindowsPowershell(), "-NoLogo", "-NoProfile", "-NonInteractive", + "-ExecutionPolicy", "Bypass", + "-Command", + "Unblock-File", + "-Path", + "\"" + executable.getAbsolutePath() + "\"", + "-ErrorAction", + "Stop" + ); + + var output = SpecsSystem.runProcess(command, true, true); + if (output.getReturnValue() == 0) { + SpecsLogs.info("Successfully unblocked dumper executable"); + } else { + SpecsLogs.info("Could not unblock dumper executable"); } - - // ClangAst executable versions are separated by an underscore - resource = resource.createResourceVersion("_" + version); - return resource; } public File getClangResourceFolder() { - return options.get(CodeParser.DUMPER_FOLDER); + return SpecsIo.mkdir(options.get(CodeParser.DUMPER_FOLDER), ClangAstWebResource.getReleaseTag()); } public static File getDefaultTempFolder() { return SpecsIo.getTempFolder(CLANG_FOLDERNAME); } - private FileResourceProvider getExecutableResource(SupportedPlatform platform) { - switch (platform) { - case WINDOWS: - return CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_EXE); - case LINUX: - if (ClangAstDumper.usePlugin()) { - return CLANG_AST_RESOURCES.get(ClangAstFileResource.LINUX_PLUGIN); - } else { - return CLANG_AST_RESOURCES.get(ClangAstFileResource.LINUX_EXE); + public static boolean useBuiltinLibc(File clangExecutable, LibcMode libcMode) { + return switch (libcMode) { + case AUTO -> !hasLibC(clangExecutable); + case BUILTIN_AND_LIBC -> true; + case SYSTEM -> false; + }; + } + + private static boolean hasLibC(File clangExecutable) { + var executableKey = SpecsIo.getCanonicalPath(clangExecutable); + return HAS_LIBC.computeIfAbsent(executableKey, ignored -> detectLibC(clangExecutable)); + } + + private static boolean detectLibC(File clangExecutable) { + File clangTest = SpecsIo.getTempFolder("clang_ast_test_" + UUID.randomUUID()); + + try { + var testFiles = List.of( + ClangAstResource.TEST_INCLUDES_C.write(clangTest), + ClangAstResource.TEST_INCLUDES_CPP.write(clangTest)); + + boolean needsLib = false; + for (var testFile : testFiles) { + var output = runClangAstDumper(clangExecutable, testFile); + + if (output.getReturnValue() != 0) { + ClavaLog.info("Problems while running dumper to test if libc/libcxx is needed"); + needsLib = true; + break; } - case MAC_OS: - return CLANG_AST_RESOURCES.get(ClangAstFileResource.MAC_OS_EXE); - default: - throw new RuntimeException("Case not defined: '" + platform + "'"); + if (testFile.getName().endsWith(".cpp") + && !output.getOutput().contains(TopLevelNodesParser.getTopLevelNodesHeader())) { + needsLib = true; + break; + } + } + + if (needsLib) { + ClavaLog.debug("Could not find system libc/libcxx"); + } else { + ClavaLog.debug("Detected system's libc and libcxx"); + } + + return !needsLib; + } finally { + SpecsIo.deleteFolder(clangTest); } } - private List getWindowsResources() { - List windowsResources = new ArrayList<>(); + private static ProcessOutputAsString runClangAstDumper(File clangExecutable, File testFile) { + List arguments = List.of(clangExecutable.getAbsolutePath(), testFile.getAbsolutePath(), "--"); + return SpecsSystem.runProcess(arguments, true, false); + } - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_DLL1)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_DLL2)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_DLL3)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_DLL4)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_DLL5)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_DLL6)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_DLL7)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_DLL8)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_DLL9)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_CLANG_DLL)); - windowsResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.WIN_LLVM_DLL)); + private List prepareIncludes(ClangDumperManifest manifest, File resourceFolder, File clangExecutable, + LibcMode libcMode) { + var useBuiltinLibc = useBuiltinLibc(clangExecutable, libcMode); + var useBuiltinCuda = options.get(CodeParser.CUDA_PATH).equalsIgnoreCase(CodeParser.getBuiltinOption()); - return windowsResources; - } + if (!useBuiltinLibc && !useBuiltinCuda) { + return List.of(); + } - private List getMacOSResources() { - List macosResources = new ArrayList<>(); + return prepareIncludes(manifest, resourceFolder); + } - macosResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.MAC_OS_LLVM_DLL)); - macosResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.MAC_OS_DLL1)); + private List prepareIncludes(ClangDumperManifest manifest, File resourceFolder) { + var extractedFolder = prepareIncludesFolder(manifest, resourceFolder); + var includeFolders = getIncludeFolders(extractedFolder); + SpecsLogs.debug(() -> "Includes folders: " + includeFolders); - return macosResources; + return includeFolders.stream().map(File::getAbsolutePath).toList(); } - private List getLinuxResources() { - List linuxResources = new ArrayList<>(); + private File prepareIncludesFolder(ClangDumperManifest manifest, File resourceFolder) { + var includesAsset = getCurrentAsset(manifest, "includes"); + var extractedFolder = new File(resourceFolder, INCLUDES_FOLDERNAME); - linuxResources.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.LINUX_LLVM_DLL)); + if (isIncludesCacheValid(extractedFolder)) { + return extractedFolder; + } + + ResourceWriteData zipFile = downloadAsset(includesAsset, resourceFolder); + + try { + SpecsIo.mkdir(extractedFolder); + SpecsIo.deleteFolderContents(extractedFolder); + SpecsIo.extractZip(zipFile.getFile(), extractedFolder); + } finally { + SpecsIo.delete(zipFile.getFile()); + } - return linuxResources; + return extractedFolder; } - private List prepareIncludes(File clangExecutable, LibcMode libcMode) { + private List getIncludeFolders(File extractedFolder) { + var entrypointsFile = new File(extractedFolder, "entrypoints.txt"); + if (!entrypointsFile.isFile()) { + throw new RuntimeException("Could not find include archive entrypoints file '" + entrypointsFile + "'"); + } - // Get base resource folder - File resourceFolder = getClangResourceFolder(); + return SpecsIo.read(entrypointsFile).lines() + .map(String::trim) + .filter(line -> !line.isEmpty()) + .map(line -> new File(extractedFolder, line)) + .toList(); + } - // Create list of include zips - List includesZips = new ArrayList<>(); + private ResourceWriteData downloadAsset(ClangDumperManifest manifest, String kind, File resourceFolder) { + var asset = getCurrentAsset(manifest, kind); + return downloadAsset(asset, resourceFolder); + } - // Get libc/libcxx resources, if required - if (useBuiltinLibc(clangExecutable, libcMode)) { + private ResourceWriteData downloadAsset(ClangDumperManifestAsset asset, File resourceFolder) { + var resource = ClangAstWebResource.getAssetResource(asset); + var writeData = resource.writeVersioned(resourceFolder, ClangResources.class); - // MacOS - if (SupportedPlatform.getCurrentPlatform().isMacOs()) { - var macosBuiltinResource = CLANG_AST_RESOURCES.get(ClangAstFileResource.LIBC_CXX_MACOS_COMPLETE); - includesZips.add(getVersionedResource(macosBuiltinResource, macosBuiltinResource.version())); - } - // Linux - else if (SupportedPlatform.getCurrentPlatform().isLinux()) { - var linuxBuiltinResource = CLANG_AST_RESOURCES.get(ClangAstFileResource.LIBC_CXX_LINUX_COMPLETE); - includesZips.add(getVersionedResource(linuxBuiltinResource, linuxBuiltinResource.version())); - } - // Windows - else if (SupportedPlatform.getCurrentPlatform().isWindows()) { - var windowsBuiltinResource = CLANG_AST_RESOURCES.get(ClangAstFileResource.LIBC_CXX_WIN32_COMPLETE); - includesZips.add(getVersionedResource(windowsBuiltinResource, windowsBuiltinResource.version())); - } else { - throw new RuntimeException("Unsupported platform: " + SupportedPlatform.getCurrentPlatform()); - } + if (!writeData.isNewFile()) { + return writeData; + } + if (!hasExpectedSha256(writeData.getFile(), asset)) { + SpecsLogs.info("Downloaded clang-dumper asset '" + asset.filename() + + "' does not match the expected checksum, downloading it again."); + SpecsIo.delete(writeData.getFile()); + writeData = resource.writeVersioned(resourceFolder, ClangResources.class); + } + if (!hasExpectedSha256(writeData.getFile(), asset)) { + throw new RuntimeException("Downloaded clang-dumper asset '" + asset.filename() + + "' does not match expected SHA-256 '" + asset.sha256() + "'"); } - // Always add OpenMP includes - includesZips.add(CLANG_AST_RESOURCES.get(ClangAstFileResource.OPENMP_INCLUDES)); + return writeData; + } - // Download includes zips, later we check if any of them is new - List zipFiles = includesZips.stream() - .map(resource -> resource.writeVersioned(resourceFolder, ClangResources.class)) - .collect(Collectors.toList()); + private ClangDumperManifestAsset getCurrentAsset(ClangDumperManifest manifest, String kind) { + var platform = getManifestPlatform(); + var arch = getManifestArch(platform); + return manifest.getAsset(platform, arch, kind); + } + static boolean isIncludesCacheValid(File includesFolder) { - var extractedFolders = new ArrayList(); + if (!includesFolder.isDirectory()) { + return false; + } - // If a new file has been written or if folder exists but is empty, delete corresponding includes folder, and extract zip again - for (var zipFile : zipFiles) { + var entrypointsFile = new File(includesFolder, "entrypoints.txt"); + if (!entrypointsFile.isFile()) { + SpecsLogs.info("Cached clang-dumper includes are missing entrypoints, extracting them again."); + return false; + } - // Obtain folder for zip - var zipFoldername = "include_" + SpecsIo.removeExtension(zipFile.getFile()); - var extractedFolder = SpecsIo.mkdir(resourceFolder, zipFoldername); + return true; + } - // Add to extracted folders list - extractedFolders.add(extractedFolder); + private void validateTopLevelCacheFiles(ClangDumperManifest manifest, File resourceFolder) { + var executableKind = ClangAstDumper.usePlugin() ? "plugin" : "tool"; + Set expectedNames = Set.of( + ClangAstWebResource.MANIFEST_FILENAME, + getCurrentAsset(manifest, executableKind).filename(), + INCLUDES_FOLDERNAME, + LAST_USED_FILENAME); + + var files = resourceFolder.listFiles(); + if (files == null) { + return; + } - // Skip extraction if zip is not new and folder is not empty - if (!zipFile.isNewFile() && !SpecsIo.isEmptyFolder(extractedFolder)) { + for (var file : files) { + if (expectedNames.contains(file.getName())) { continue; } - // Clean folder - SpecsIo.deleteFolderContents(extractedFolder); + SpecsLogs.info("Deleting unexpected file from clang-dumper cache: " + file); + SpecsIo.delete(file); + } + } - // Extract zip contents to folder - SpecsIo.extractZip(zipFile.getFile(), extractedFolder); + private void updateLastUsedAndCleanupStaleVersions(File resourceFolder) { + var now = Instant.now(); + writeLastUsed(resourceFolder, now); + + var staleCleanup = new Thread(() -> deleteStaleVersions(now, resourceFolder), + "clang-dumper-stale-cache-cleanup"); + staleCleanup.setDaemon(true); + staleCleanup.start(); + } + + private static void writeLastUsed(File resourceFolder, Instant timestamp) { + writeTimestamp(new File(resourceFolder, LAST_USED_FILENAME), timestamp); + } + + private static void writeTimestamp(File file, Instant timestamp) { + SpecsIo.write(file, timestamp.toString()); + } + + void deleteStaleVersions(Instant now, File currentVersionFolder) { + File cacheBaseFolder = options.get(CodeParser.DUMPER_FOLDER); + var versions = cacheBaseFolder.listFiles(File::isDirectory); + if (versions == null) { + return; } - // Add all folders inside extracted folders as system include - var includesFiles = new ArrayList(); - for (var extractedFolder : extractedFolders) { - var includeFolders = SpecsIo.getFolders(extractedFolder); + for (var versionFolder : versions) { + if (versionFolder.getAbsoluteFile().equals(currentVersionFolder.getAbsoluteFile())) { + continue; + } - includesFiles.addAll(includeFolders); + var jvmLock = CLANG_FILES_LOCKS.computeIfAbsent(versionFolder.getAbsolutePath(), ignored -> new Object()); + try { + synchronized (jvmLock) { + var lastUsedFile = new File(versionFolder, LAST_USED_FILENAME); + if (!lastUsedFile.isFile()) { + continue; + } + + try (var lock = tryAcquireCacheLock(versionFolder)) { + if (lock == null) { + SpecsLogs.debug(() -> "Skipping locked clang-dumper cache folder: " + versionFolder); + continue; + } + + if (!lastUsedFile.isFile()) { + continue; + } + + var lastUsed = Instant.parse(SpecsIo.read(lastUsedFile).trim()); + if (lastUsed.isBefore(now.minus(STALE_CACHE_MAX_AGE))) { + SpecsLogs.info("Deleting stale clang-dumper cache folder: " + versionFolder); + SpecsIo.deleteFolder(versionFolder); + } + } + } + } catch (IOException | RuntimeException e) { + SpecsLogs.warn("Could not inspect clang-dumper cache folder '" + versionFolder + "'", e); + } } + } + + static CacheLock acquireCacheLock(File versionFolder) throws IOException { + return acquireCacheLock(versionFolder, true); + } + private static CacheLock tryAcquireCacheLock(File versionFolder) throws IOException { + return acquireCacheLock(versionFolder, false); + } + + private static CacheLock acquireCacheLock(File versionFolder, boolean wait) throws IOException { + var lockFolder = getCacheLockFolder(versionFolder); + Files.createDirectories(lockFolder.getParentFile().toPath()); + + while (true) { + try { + Files.createDirectory(lockFolder.toPath()); + } catch (FileAlreadyExistsException e) { + if (!isCacheLockStale(lockFolder)) { + if (!wait) { + return null; + } + + waitForCacheLock(); + continue; + } + + recoverStaleCacheLock(lockFolder); + continue; + } - // Sort them alphabetically, by last foldername, include order is important - Collections.sort(includesFiles, Comparator.comparing(File::getName)); - SpecsLogs.debug(() -> "Includes folders: " + includesFiles); + var ownerFile = new File(lockFolder, CACHE_LOCK_OWNER_PREFIX + UUID.randomUUID()); + try { + Files.writeString(ownerFile.toPath(), getProcessIdentity(), StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE); + } catch (NoSuchFileException e) { + // Stale-lock recovery removed the directory while this process was claiming it. + continue; + } catch (IOException e) { + deleteEmptyCacheLock(lockFolder); + throw e; + } - return includesFiles.stream().map(File::getAbsolutePath).toList(); + return new CacheLock(lockFolder, ownerFile); + } } - public static boolean useBuiltinLibc(File clangExecutable, LibcMode libcMode) { + /** + * Returns the temporary lock folder for a cache version. + * + *

The lock folder is outside the version folder because stale cleanup deletes that folder while holding the + * lock. The folder is removed when the lock is released, so normal operation leaves no lock artifact behind.

+ */ + static File getCacheLockFolder(File versionFolder) { + var absoluteVersionFolder = versionFolder.getAbsoluteFile(); + return new File(absoluteVersionFolder.getParentFile(), absoluteVersionFolder.getName() + CACHE_LOCK_FOLDERNAME); + } - return switch (libcMode) { - case AUTO -> !hasLibC(clangExecutable); - case BUILTIN_AND_LIBC -> true; - case SYSTEM -> false; - }; + private static String getProcessIdentity() { + var process = ProcessHandle.current(); + var startTime = process.info().startInstant().map(Instant::toString).orElse(""); + return process.pid() + System.lineSeparator() + startTime; } - private static boolean hasLibC(File clangExecutable) { - var value = HAS_LIBC.get(); + private static boolean isCacheLockStale(File lockFolder) throws IOException { + if (!lockFolder.exists()) { + return true; + } - // Check if initiallized - if (value == -1) { - var hasLibC = detectLibC(clangExecutable); - value = hasLibC ? 1 : 0; - HAS_LIBC.set(value); + if (!lockFolder.isDirectory()) { + throw new IOException("Cache lock path is not a directory: '" + lockFolder + "'"); } - if (value == 0) { - return false; + var ownerFiles = lockFolder.listFiles(File::isFile); + if (ownerFiles == null) { + if (!lockFolder.exists()) { + return true; + } + + throw new IOException("Could not list cache lock folder: '" + lockFolder + "'"); } - if (value == 1) { - return true; + if (ownerFiles.length == 0) { + return isCacheLockOld(lockFolder); + } + + for (var ownerFile : ownerFiles) { + if (!isCacheLockOwnerStale(lockFolder, ownerFile)) { + return false; + } } - throw new RuntimeException("Unexpected value: '" + value + "'"); + return true; } - /** - * Detects if the system has libc/licxx installed. - * - * @param clangExecutable - * @return - */ - private static boolean detectLibC(File clangExecutable) { + private static boolean isCacheLockOwnerStale(File lockFolder, File ownerFile) throws IOException { + List lines; + try { + lines = Files.readAllLines(ownerFile.toPath()); + } catch (NoSuchFileException e) { + return true; + } - File clangTest = SpecsIo.mkdir(SpecsIo.getTempFolder(), "clang_ast_test"); + if (lines.isEmpty()) { + return isCacheLockOld(lockFolder); + } - // Write test files - List testFiles = Arrays.asList(ClangAstResource.TEST_INCLUDES_C, ClangAstResource.TEST_INCLUDES_CPP) - .stream() - .map(resource -> resource.write(clangTest)) - .collect(Collectors.toList()); + try { + var pid = Long.parseLong(lines.get(0).trim()); + var process = ProcessHandle.of(pid); + if (process.isEmpty() || !process.get().isAlive()) { + return true; + } - boolean needsLib = false; - for (File testFile : testFiles) { + if (lines.size() > 1 && !lines.get(1).isBlank()) { + var processStart = process.get().info().startInstant(); + if (processStart.isPresent() && !processStart.get().toString().equals(lines.get(1).trim())) { + return true; + } + } - // Invoke dumper - var output = runClangAstDumper(clangExecutable, testFile); + return false; + } catch (NumberFormatException e) { + return isCacheLockOld(lockFolder); + } + } - // First check if there where no problems running the dumper - if (output.getReturnValue() != 0) { - ClavaLog.info("Problems while running dumper to test in libc/libcxx is needed"); - needsLib = true; - break; - } + private static boolean isCacheLockOld(File lockFolder) throws IOException { + try { + return Files.getLastModifiedTime(lockFolder.toPath()).toInstant() + .isBefore(Instant.now().minus(CACHE_LOCK_STALE_MAX_AGE)); + } catch (NoSuchFileException e) { + return true; + } + } - // Test files where built in such a way so that if a system include is present, it will generate code with a - // top level nodes, otherwise it generates an empty file - var topLevelNodesHeader = TopLevelNodesParser.getTopLevelNodesHeader(); + private static void waitForCacheLock() throws IOException { + try { + Thread.sleep(CACHE_LOCK_RETRY_INTERVAL.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for clang-dumper cache lock", e); + } + } + + private static void recoverStaleCacheLock(File lockFolder) throws IOException { + if (!lockFolder.isDirectory()) { + return; + } - var foundInclude = output.getOutput().contains(topLevelNodesHeader); + var ownerFiles = lockFolder.listFiles(File::isFile); + if (ownerFiles == null) { + return; + } - if (!foundInclude) { - needsLib = true; - break; + for (var ownerFile : ownerFiles) { + if (isCacheLockOwnerStale(lockFolder, ownerFile)) { + Files.deleteIfExists(ownerFile.toPath()); } + } + deleteEmptyCacheLock(lockFolder); + } + + private static void deleteEmptyCacheLock(File lockFolder) throws IOException { + try { + Files.deleteIfExists(lockFolder.toPath()); + } catch (DirectoryNotEmptyException e) { + // A replacement owner claimed the lock while stale recovery was in progress. } + } - if (needsLib) { - ClavaLog.debug("Could not find system libc/libcxx"); - } else { - ClavaLog.debug("Detected system's libc and libcxx"); + /** + * A temporary claim on one cache version. Each claim has its own owner marker, so releasing an old claim cannot + * remove a newer claim created after stale-lock recovery. + */ + static final class CacheLock implements AutoCloseable { + + private final File lockFolder; + private final File ownerFile; + + private CacheLock(File lockFolder, File ownerFile) { + this.lockFolder = lockFolder; + this.ownerFile = ownerFile; } - return !needsLib; + File ownerFile() { + return ownerFile; + } + @Override + public void close() { + try { + Files.deleteIfExists(ownerFile.toPath()); + deleteEmptyCacheLock(lockFolder); + } catch (IOException e) { + SpecsLogs.warn("Could not remove temporary clang-dumper cache lock '" + lockFolder + "'", e); + } + } } - private static ProcessOutputAsString runClangAstDumper(File clangExecutable, File testFile) { - List arguments = Arrays.asList(clangExecutable.getAbsolutePath(), testFile.getAbsolutePath(), "--"); - return SpecsSystem.runProcess(arguments, true, false); + private static boolean hasExpectedSha256(File file, ClangDumperManifestAsset asset) { + return asset.sha256().equalsIgnoreCase(calculateSha256(file)); } - public File getBuiltinCudaLib() { - return cudalibFolder.get(); + private static String calculateSha256(File file) { + try { + var digest = MessageDigest.getInstance("SHA-256"); + try (var inputStream = new DigestInputStream(Files.newInputStream(file.toPath()), digest)) { + inputStream.transferTo(OutputStream.nullOutputStream()); + } + + return HexFormat.of().formatHex(digest.digest()); + } catch (IOException | NoSuchAlgorithmException e) { + throw new RuntimeException("Could not calculate SHA-256 for file '" + file + "'", e); + } } - private File prepareBuiltinCudaLib() { - var fileResource = CLANG_AST_RESOURCES.get(ClangAstFileResource.CUDA_LIB); - var resourceFolder = getClangResourceFolder(); - var cudalibFolder = SpecsIo.mkdir(new File(resourceFolder, "cudalib")); + private static String getManifestPlatform() { + var platform = SupportedPlatform.getCurrentPlatform(); - // Download includes zips, check if any of them is new - ResourceWriteData zipFile = fileResource.writeVersioned(resourceFolder, ClangResources.class); + if (platform.isLinux()) { + return "linux"; + } - // If a new file has been written, delete includes folder, and extract all zips again - // Extracting all because zips might have several folders and we are not determining which should be updated - if (zipFile.isNewFile()) { - // Clean folder - SpecsIo.deleteFolderContents(cudalibFolder); + if (platform.isMacOs()) { + return "macos"; + } - // Extract zip - SpecsIo.extractZip(zipFile.getFile(), cudalibFolder); + if (platform.isWindows()) { + return "windows"; } - // Returnb cuda lib folder - return cudalibFolder; + throw new RuntimeException("Unsupported platform: " + platform); } + + private static String getManifestArch(String platform) { + var osArch = System.getProperty("os.arch").toLowerCase(); + + if (osArch.equals("amd64") || osArch.equals("x86_64")) { + return platform.equals("windows") ? "x86_64" : "x64"; + } + + if (osArch.equals("aarch64") || osArch.equals("arm64")) { + return "arm64"; + } + + throw new RuntimeException("Unsupported architecture for clang-dumper: " + osArch); + } + } diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/SupportedPlatform.java b/ClangAstParser/src/pt/up/fe/specs/clang/SupportedPlatform.java index 094c8662e..0564571a8 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/SupportedPlatform.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/SupportedPlatform.java @@ -74,10 +74,6 @@ private static SupportedPlatform calculateCurrentPlatform() { // Linux if (SpecsPlatforms.isLinux()) { - if (SpecsPlatforms.isLinuxArm()) { - throw new RuntimeException("ARM-based platforms are not currently supported"); - } - return LINUX; } diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/CodeParser.java b/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/CodeParser.java index 5c154cd24..f7441ed77 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/CodeParser.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/CodeParser.java @@ -45,7 +45,7 @@ public abstract class CodeParser extends ADataClass { .setLabel("CUDA Path (empty: uses system installed; : uses builtin version)") .setDefaultString(""); public static final DataKey DUMPER_FOLDER = KeyFactory.folder("dumperFolder") - .setLabel("The work folder for the clang-dumper. Clava will look for it in this folder, and if not found, will download it. If not set, a temporary folder will be used.") + .setLabel("The base cache folder for the clang-dumper. Clava stores each dumper release in a versioned subfolder and downloads it if not found. If not set, a temporary folder will be used.") .setDefault(ClangResources::getDefaultTempFolder); /** diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/ParallelCodeParser.java b/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/ParallelCodeParser.java index 399d91621..d16c2ce82 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/ParallelCodeParser.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/codeparser/ParallelCodeParser.java @@ -104,8 +104,6 @@ public App parse(List inputSources, List compilerOptions, ClavaCon // Standard standard = getStandard(allUserSources.values(), options); // config.getTry(ClavaOptions.STANDARD).ifPresent(standard -> arguments.add(standard.getFlag())); - // Get version for the executable - String version = options.get(ClangAstKeys.CLANGAST_VERSION); // System.out.println("PARALLEL OPTIONS: " + options); // Prepare resources before execution // ClangResources clangResources = new ClangResources(get(SHOW_CLANG_DUMP)); @@ -117,7 +115,7 @@ public App parse(List inputSources, List compilerOptions, ClavaCon ClavaLog.debug(() -> "In Linux, ClangAstDumper is a plugin. LIBC_CXX_MODE is reset to SYSTEM."); } - var clangFiles = clangResources.getClangFiles(version, get(ClangAstKeys.LIBC_CXX_MODE)); + var clangFiles = clangResources.getClangFiles(get(ClangAstKeys.LIBC_CXX_MODE)); // File clangExecutable = clangResources.prepareResources(version); // List builtinIncludes = clangResources.prepareIncludes(clangExecutable, // get(ClangAstKeys.USE_PLATFORM_INCLUDES)); @@ -375,8 +373,7 @@ private ClangAstData parseSource(File sourceFile, String id, Standard standard, // Only show output of console after parsing is done, when using parallel parsing boolean streamConsoleOutput = !get(PARALLEL_PARSING); - ClangAstDumper clangParser = new ClangAstDumper(streamConsoleOutput, clangExecutable, builtinIncludes, - this) + ClangAstDumper clangParser = new ClangAstDumper(streamConsoleOutput, clangExecutable, builtinIncludes, this) .setBaseFolder(parsingFolder) .setSystemIncludesThreshold(get(SYSTEM_INCLUDES_THRESHOLD)); diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstDumper.java b/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstDumper.java index 0a319a6d8..ba7f29de3 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstDumper.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstDumper.java @@ -52,6 +52,7 @@ public class ClangAstDumper { private final static boolean USE_PLUGIN = false; + private final static String SYSTEM_HEADER_THRESHOLD_OPTION = "-system-header-threshold="; public static boolean usePlugin() { return USE_PLUGIN; @@ -60,7 +61,7 @@ public static boolean usePlugin() { private final static String CLANG_DUMP_FILENAME = "clangDump.txt"; private final static String STDERR_DUMP_FILENAME = "stderr.txt"; - private static final List CLANG_AST_DUMPER_TEMP_FILES = Arrays.asList("includes.txt", CLANG_DUMP_FILENAME, + private static final List CLANG_AST_DUMPER_TEMP_FILES = List.of("includes.txt", CLANG_DUMP_FILENAME, // "clavaDump.txt", "nodetypes.txt", "types.txt", "is_temporary.txt", "template_args.txt", "clavaDump.txt", "nodetypes.txt", "types.txt", "is_temporary.txt", "omp.txt", "invalid_source.txt", "enum_integer_type.txt", "consumer_order.txt", @@ -86,7 +87,7 @@ public static List getTempFiles() { private File clangExecutable; private List builtinIncludes; private int systemIncludesThreshold; - private ClangResources clangResources; + private final ClangResources clangResources; private final CodeParser parserConfig; @@ -168,7 +169,7 @@ private ClangAstData parsePrivate(File sourceFile, String id, Standard standard, arguments.add("-Xclang"); arguments.add("-plugin-arg-DumpAst"); arguments.add("-Xclang"); - arguments.add("-system-threshold=" + systemIncludesThreshold); + arguments.add(SYSTEM_HEADER_THRESHOLD_OPTION + systemIncludesThreshold); } else { arguments.add(clangExecutable.getAbsolutePath()); @@ -176,7 +177,7 @@ private ClangAstData parsePrivate(File sourceFile, String id, Standard standard, arguments.add("-id=" + id); - arguments.add("-system-header-threshold=" + systemIncludesThreshold); + arguments.add(SYSTEM_HEADER_THRESHOLD_OPTION + systemIncludesThreshold); arguments.add("--"); } @@ -193,8 +194,10 @@ private ClangAstData parsePrivate(File sourceFile, String id, Standard standard, arguments.add("-std=cl2.0"); } // Set standard to CUDA - else if (isCuda && !standard.isCuda()) { - arguments.add("-std=cuda"); + else if (isCuda) { + // The LLVM 18 driver bundled with clang-dumper rejects '-std=cuda'. The .cu extension already + // selects CUDA mode, so use a C++ standard for host-side parsing. + arguments.add(standard.isCxx() ? standard.getFlag() : Standard.CXX17.getFlag()); } else { arguments.add(standard.getFlag()); } @@ -223,24 +226,19 @@ else if (isCuda && !standard.isCuda()) { } // If CUDA, add corresponding flags else if (isCuda) { - if (SpecsPlatforms.isWindows()) { - ClavaLog.info("CUDA parsing is not supported in Windows, run at your own risk"); - arguments.addAll(Arrays.asList("-fms-compatibility", "-D_MSC_VER", "-D_LIBCPP_MSVCRT")); + if (!SpecsPlatforms.isLinux()) { + ClavaLog.info("We only officially support CUDA parsing in Linux, run at your own risk"); + arguments.add("-fms-compatibility"); + if (SpecsPlatforms.isWindows()) { + arguments.add("-D_MSC_VER"); + arguments.add("-D_LIBCPP_MSVCRT"); + } } arguments.add("--cuda-gpu-arch=" + parserConfig.get(CodeParser.CUDA_GPU_ARCH)); var cudaPath = parserConfig.get(CodeParser.CUDA_PATH); - if (!cudaPath.isBlank()) { - - // Check if should use built-in CUDA lib - File cudaFolder = cudaPath.toUpperCase().equals(CodeParser.getBuiltinOption()) - ? clangResources.getBuiltinCudaLib() - : SpecsIo.existingFolder(cudaPath); - - ClavaLog.debug("Setting --cuda-path to folder '" + cudaFolder.getAbsolutePath() + "'"); - arguments.add("--cuda-path=" + cudaFolder.getAbsolutePath()); - } + addCudaPathArgument(arguments, cudaPath); // Since we only need parsing, enable host-only // Can help with errors such as "__float128 is not supported on this target" @@ -299,7 +297,7 @@ else if (SourceType.isHeader(sourceFile)) { workingFolders.add(lastWorkingFolder); output = SpecsSystem.runProcess(arguments, lastWorkingFolder, - inputStream -> this.processOutput(sourceFile, inputStream), + this::processOutput, inputStream -> this.processStdErr(inputStream, config.get(ClavaNode.CONTEXT))); if (output.isError()) { @@ -336,7 +334,24 @@ else if (SourceType.isHeader(sourceFile)) { return parsedData; } - private String processOutput(File sourceFile, InputStream inputStream) { + private void addCudaPathArgument(List arguments, String cudaPath) { + var useBuiltinCudaLib = cudaPath.toUpperCase().equals(CodeParser.getBuiltinOption()); + + if (useBuiltinCudaLib) { + File cudaFolder = clangResources.getBuiltinCudaLib(); + + ClavaLog.debug("Setting --cuda-path to built-in CUDA folder '" + + cudaFolder.getAbsolutePath() + "'"); + arguments.add("--cuda-path=" + cudaFolder.getAbsolutePath()); + } else if (!cudaPath.isBlank()) { + File cudaFolder = SpecsIo.existingFolder(cudaPath); + + ClavaLog.debug("Setting --cuda-path to folder '" + cudaFolder.getAbsolutePath() + "'"); + arguments.add("--cuda-path=" + cudaFolder.getAbsolutePath()); + } + } + + private String processOutput(InputStream inputStream) { StringBuilder output = new StringBuilder(); try (LineStream lines = LineStream.newInstance(inputStream, null)) { diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstParser.java b/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstParser.java index 23d605105..ccfb24385 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstParser.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/dumper/ClangAstParser.java @@ -147,7 +147,6 @@ public TranslationUnit parseTu(File sourceFile) { } ClavaNode parsedNode = data.get(ClangAstData.CLAVA_NODES).get(topLevelTypeId); Objects.requireNonNull(parsedNode, () -> "No node for type '" + topLevelTypeId + "'"); - } // Parse top-level attributes diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/ClavaNodeParser.java b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/ClavaNodeParser.java index 0cfe3473f..19d3bfbcf 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/ClavaNodeParser.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/ClavaNodeParser.java @@ -171,10 +171,6 @@ private ClavaNode parseNode(String nodeId, String classname, ClangAstData data, if (nodeData == null) { throw new RuntimeException("No ClavaData/DataStore for node '" + nodeId + "' (classname: " + classname + "), data dumper is not being called (linestream index '" + lineStream.getLastLineIndex() + "')"); - // if (debug) - // SpecsLogs.msgInfo("No ClavaData for node '" + nodeId + "' (classname: " + classname - // + "), data dumper is not being called"); - // return new UnsupportedNode(classname, ClavaData.empty(), Collections.emptyList()); } // Get corresponding ClavaNode class @@ -240,8 +236,7 @@ private ClavaNode parseNode(String nodeId, String classname, ClangAstData data, int index = i; Objects.requireNonNull(child, () -> "Did not find ClavaNode for child with index '" + index + "' and id '" + childId - + "' when parsing " - + clavaNodeClass.getSimpleName() + " -> " + nodeData); + + "' when parsing " + clavaNodeClass.getSimpleName() + " -> " + nodeData); child = processChild(child, clavaNodeClass, data); diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/ClavaNodes.java b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/ClavaNodes.java index 964fe3889..30438176c 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/ClavaNodes.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/ClavaNodes.java @@ -148,9 +148,9 @@ public void queueSetNode(DataClass data, DataKey key + "', if node can be null, use queueOptional instead. Node data:\n" + data); } - // Get node - ClavaNode node = get(nodeId); - + ClavaNode node = Objects.requireNonNull(clavaNodes.get(nodeId), + () -> "Could not resolve required node '" + nodeId + "' for key '" + key.getName() + + "'. Node data:\n" + data); Class valueClass = key.getValueClass(); ClavaNode adaptedNode = adaptNode(node, valueClass); @@ -199,8 +199,11 @@ public void queueSetOptionalNode(DataClass data, DataKe Runnable nodeToAdd = () -> { + @SuppressWarnings("unchecked") Optional value = isNullId(nodeId) ? Optional.empty() - : key.getValueClass().cast(getOptional(nodeId)); + : Optional.of((T) Objects.requireNonNull(clavaNodes.get(nodeId), + () -> "Could not resolve optional node '" + nodeId + "' for key '" + key.getName() + + "'. Node data:\n" + data)); data.set(key, value); }; @@ -213,7 +216,10 @@ public void queueSetNullableNode(DataClass data, DataKe Runnable nodeToAdd = () -> { - ClavaNode value = isNullId(nodeId) ? getNullNodeType(nodeId).newNullNode(factory) : get(nodeId); + ClavaNode value = isNullId(nodeId) ? getNullNodeType(nodeId).newNullNode(factory) + : Objects.requireNonNull(clavaNodes.get(nodeId), + () -> "Could not resolve nullable node '" + nodeId + "' for key '" + key.getName() + + "'. Node data:\n" + data); data.set(key, key.getValueClass().cast(value)); }; @@ -227,7 +233,11 @@ public void queueSetNodeList(DataClass data, DataKey
  • { @SuppressWarnings("unchecked") // If the nodes exist, they should be of the requested type - List nodes = nodeIds.stream().map(id -> (T) get(id)).collect(Collectors.toList()); + List nodes = nodeIds.stream() + .map(id -> (T) Objects.requireNonNull(clavaNodes.get(id), + () -> "Could not resolve node '" + id + "' in list for key '" + key.getName() + + "'. Node data:\n" + data)) + .collect(Collectors.toList()); data.set(key, nodes); }; diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/LanguageParser.java b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/LanguageParser.java index e366767bc..3524f18ba 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/LanguageParser.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/LanguageParser.java @@ -55,6 +55,7 @@ public void apply(LineStream lineStream, ClangAstData data) { .set(Language.C_PLUS_PLUS_17, LineStreamParsers.oneOrZero(lineStream)) .set(Language.C_PLUS_PLUS_20, LineStreamParsers.oneOrZero(lineStream)) .set(Language.C_PLUS_PLUS_23, LineStreamParsers.oneOrZero(lineStream)) + .set(Language.C_PLUS_PLUS_26, LineStreamParsers.oneOrZero(lineStream)) .set(Language.HAS_DIGRAPHS, LineStreamParsers.oneOrZero(lineStream)) .set(Language.IS_GNU, LineStreamParsers.oneOrZero(lineStream)) .set(Language.HEX_FLOATS, LineStreamParsers.oneOrZero(lineStream)) diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/data/ExprDataParser.java b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/data/ExprDataParser.java index 3687d9e62..858213516 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/data/ExprDataParser.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/data/ExprDataParser.java @@ -119,7 +119,7 @@ public static DataStore parseCharacterLiteralData(LineStream lines, ClangAstData DataStore data = parseLiteralData(lines, dataStore); data.add(CharacterLiteral.VALUE, LineStreamParsers.longInt(lines)); - data.add(CharacterLiteral.KIND, LineStreamParsers.enumFromInt(CharacterKind.getEnumHelper(), lines)); + data.add(CharacterLiteral.KIND, LineStreamParsers.enumFromName(CharacterKind.getEnumHelper(), lines)); return data; } diff --git a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/data/TypeDataParser.java b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/data/TypeDataParser.java index 2dbe71624..48d743ff2 100644 --- a/ClangAstParser/src/pt/up/fe/specs/clang/parsers/data/TypeDataParser.java +++ b/ClangAstParser/src/pt/up/fe/specs/clang/parsers/data/TypeDataParser.java @@ -366,7 +366,7 @@ public static DataStore parseUnaryTransformTypeData(LineStream lines, ClangAstDa DataStore data = parseTypeData(lines, parserData); data.add(UnaryTransformType.KIND, LineStreamParsers.enumFromName(UnaryTransformTypeKind.class, lines)); - parserData.getClavaNodes().queueSetNode(data, UnaryTransformType.UNDERLYING_TYPE, lines.nextLine()); + parserData.getClavaNodes().queueSetOptionalNode(data, UnaryTransformType.UNDERLYING_TYPE, lines.nextLine()); parserData.getClavaNodes().queueSetNode(data, UnaryTransformType.BASE_TYPE, lines.nextLine()); return data; diff --git a/ClangAstParser/test-resources/cxx/dependent_scope_decl_ref_expr.cpp.txt b/ClangAstParser/test-resources/cxx/dependent_scope_decl_ref_expr.cpp.txt index cce55922e..5b84d42dc 100644 --- a/ClangAstParser/test-resources/cxx/dependent_scope_decl_ref_expr.cpp.txt +++ b/ClangAstParser/test-resources/cxx/dependent_scope_decl_ref_expr.cpp.txt @@ -7,5 +7,6 @@ void compute_boundaries(FloatType value) { // value = 0.F * 2^(1 - bias) = ( F) * 2^(1 - bias - (p-1)) // If v is normalized: // value = 1.F * 2^(E - bias) = (2^(p-1) + F) * 2^(E - bias - (p-1)) - static_assert(std::numeric_limits::is_iec559, "internal error: dtoa_short requires an IEEE-754 floating-point implementation"); + static_assert(std::numeric_limits::is_iec559, "internal error: dtoa_short requires an IEEE-754 " + "floating-point implementation"); } diff --git a/ClangAstParser/test-resources/cxx/paren_list_initialization.cpp b/ClangAstParser/test-resources/cxx/paren_list_initialization.cpp new file mode 100644 index 000000000..8b84bb9cf --- /dev/null +++ b/ClangAstParser/test-resources/cxx/paren_list_initialization.cpp @@ -0,0 +1,8 @@ +struct Point { + int x; + int y; +}; + +void test() { + Point point(1, 2); +} diff --git a/ClangAstParser/test-resources/cxx/source_locations.cpp b/ClangAstParser/test-resources/cxx/source_locations.cpp new file mode 100644 index 000000000..a80c933e5 --- /dev/null +++ b/ClangAstParser/test-resources/cxx/source_locations.cpp @@ -0,0 +1,17 @@ +#define VALUE 7 +#define CAT_IMPL(left, right) left##right +#define CAT(left, right) CAT_IMPL(left, right) +#define DECL(name) int name = VALUE; + +DECL(CAT(macro_, value)) +int ordinary = 0; +int foobar = 1; +int pasted_reference = CAT(foo, bar); + +namespace std { +using uint8_t = unsigned char; +template class vector; +} // namespace std + +template > +class Holder {}; diff --git a/ClangAstParser/test-resources/cxx/unevaluated_strings.cpp b/ClangAstParser/test-resources/cxx/unevaluated_strings.cpp new file mode 100644 index 000000000..b808d729b --- /dev/null +++ b/ClangAstParser/test-resources/cxx/unevaluated_strings.cpp @@ -0,0 +1,3 @@ +static_assert(true, "plain message"); +static_assert(true, "line\nbreak"); +static_assert(true, "\u00e9"); diff --git a/ClangAstParser/test-resources/cxx/unevaluated_strings.cpp.txt b/ClangAstParser/test-resources/cxx/unevaluated_strings.cpp.txt new file mode 100644 index 000000000..b808d729b --- /dev/null +++ b/ClangAstParser/test-resources/cxx/unevaluated_strings.cpp.txt @@ -0,0 +1,3 @@ +static_assert(true, "plain message"); +static_assert(true, "line\nbreak"); +static_assert(true, "\u00e9"); diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java new file mode 100644 index 000000000..7da6ca019 --- /dev/null +++ b/ClangAstParser/test/pt/up/fe/specs/clang/ClangResourcesTest.java @@ -0,0 +1,672 @@ +/** + * Copyright 2026 SPeCS. + *

    + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

    + * http://www.apache.org/licenses/LICENSE-2.0 + *

    + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package pt.up.fe.specs.clang; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import pt.up.fe.specs.clang.ClangAstWebResource.LocalBuild; +import pt.up.fe.specs.clang.ClangAstWebResource.Release; +import pt.up.fe.specs.clang.codeparser.CodeParser; +import pt.up.fe.specs.clang.parsers.TopLevelNodesParser; + +public class ClangResourcesTest { + + private static final Duration PROCESS_TIMEOUT = Duration.ofSeconds(10); + + @TempDir + Path tempFolder; + + @Test + public void releaseTagIsParsedAsRelease() { + var release = assertInstanceOf(Release.class, ClangAstWebResource.parseDumperSource("v16.0.5_3")); + + assertEquals("v16.0.5_3", release.tag()); + } + + @Test + public void absolutePathIsParsedAsLocalBuild() { + var localBuild = assertInstanceOf(LocalBuild.class, + ClangAstWebResource.parseDumperSource(tempFolder.toString())); + + assertEquals(tempFolder.toFile(), localBuild.folder()); + } + + @Test + public void relativePathIsRejected() { + assertThrows(RuntimeException.class, + () -> ClangAstWebResource.parseDumperSource("../clang-dumper/build")); + } + + @Test + public void localBuildSelectsExpectedTool() throws IOException { + var toolName = SupportedPlatform.getCurrentPlatform().isWindows() ? "tool.exe" : "tool"; + var tool = tempFolder.resolve(toolName).toFile(); + assertTrue(tool.createNewFile()); + + assertEquals(tool, ClangResources.getLocalExecutable(tempFolder.toFile())); + } + + @Test + public void localBuildRequiresExpectedTool() { + assertThrows(RuntimeException.class, () -> ClangResources.getLocalExecutable(tempFolder.toFile())); + } + + @Test + public void includesCacheValidationOnlyChecksRequiredFiles() throws IOException { + var includesFolder = tempFolder.resolve("includes"); + assertFalse(ClangResources.isIncludesCacheValid(includesFolder.toFile())); + + Files.createDirectory(includesFolder); + assertFalse(ClangResources.isIncludesCacheValid(includesFolder.toFile())); + + Files.createDirectories(includesFolder.resolve("builtin")); + Files.writeString(includesFolder.resolve("entrypoints.txt"), "builtin\n"); + Files.writeString(includesFolder.resolve("builtin/header.h"), "original"); + Files.writeString(includesFolder.resolve("unexpected.txt"), "extra"); + + assertTrue(ClangResources.isIncludesCacheValid(includesFolder.toFile())); + + Files.writeString(includesFolder.resolve("builtin/header.h"), "modified"); + assertTrue(ClangResources.isIncludesCacheValid(includesFolder.toFile())); + } + + @Test + public void staleCacheCleanupSkipsLockedVersions() throws IOException { + var currentVersion = Files.createDirectory(tempFolder.resolve("current")).toFile(); + var staleVersion = Files.createDirectory(tempFolder.resolve("stale")).toFile(); + Files.writeString(staleVersion.toPath().resolve("last-used.txt"), + Instant.now().minus(Duration.ofDays(61)).toString()); + + var parser = CodeParser.newInstance(); + parser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); + var resources = new ClangResources(parser); + + try (var ignored = ClangResources.acquireCacheLock(staleVersion)) { + + resources.deleteStaleVersions(Instant.now(), currentVersion); + assertTrue(staleVersion.isDirectory()); + } + + assertFalse(ClangResources.getCacheLockFolder(staleVersion).exists()); + + resources.deleteStaleVersions(Instant.now(), currentVersion); + assertFalse(staleVersion.exists()); + } + + @Test + public void cacheLockSerializesConcurrentAcquisition() throws Exception { + var versionFolder = Files.createDirectory(tempFolder.resolve("version")).toFile(); + var executor = Executors.newSingleThreadExecutor(); + ClangResources.CacheLock firstLock = ClangResources.acquireCacheLock(versionFolder); + + try { + var secondLock = executor.submit(() -> ClangResources.acquireCacheLock(versionFolder)); + assertFalse(secondLock.isDone()); + + firstLock.close(); + firstLock = null; + try (var ignored = secondLock.get(10, TimeUnit.SECONDS)) { + assertTrue(versionFolder.isDirectory()); + } + } finally { + if (firstLock != null) { + firstLock.close(); + } + executor.shutdownNow(); + executor.awaitTermination(10, TimeUnit.SECONDS); + } + + assertFalse(ClangResources.getCacheLockFolder(versionFolder).exists()); + } + + @Test + public void cacheLockSerializesAcquisitionAcrossJvms() throws Exception { + var versionFolder = Files.createDirectory(tempFolder.resolve("version")).toFile(); + var firstAcquired = tempFolder.resolve("first.acquired"); + var firstRelease = tempFolder.resolve("first.release"); + var secondAcquired = tempFolder.resolve("second.acquired"); + var secondRelease = tempFolder.resolve("second.release"); + var firstLog = tempFolder.resolve("first.log"); + var secondLog = tempFolder.resolve("second.log"); + + Process first = startCacheLockProcess(versionFolder, firstAcquired, firstRelease, firstLog); + Process second = null; + try { + assertTrue(waitForFile(firstAcquired, PROCESS_TIMEOUT)); + + second = startCacheLockProcess(versionFolder, secondAcquired, secondRelease, secondLog); + assertFalse(waitForFile(secondAcquired, Duration.ofMillis(750)), + "A second JVM acquired a cache lock that was still held"); + + Files.createFile(firstRelease); + assertTrue(waitForFile(secondAcquired, PROCESS_TIMEOUT)); + + Files.createFile(secondRelease); + waitForProcess(first, firstLog); + waitForProcess(second, secondLog); + } finally { + releaseProcess(firstRelease); + releaseProcess(secondRelease); + stopProcess(first); + stopProcess(second); + } + + assertFalse(ClangResources.getCacheLockFolder(versionFolder).exists()); + } + + @Test + public void cacheLockAllowsDifferentVersionsToProceedAcrossJvms() throws Exception { + var firstVersion = Files.createDirectory(tempFolder.resolve("version-1")).toFile(); + var secondVersion = Files.createDirectory(tempFolder.resolve("version-2")).toFile(); + var firstAcquired = tempFolder.resolve("first.acquired"); + var firstRelease = tempFolder.resolve("first.release"); + var secondAcquired = tempFolder.resolve("second.acquired"); + var secondRelease = tempFolder.resolve("second.release"); + var firstLog = tempFolder.resolve("first.log"); + var secondLog = tempFolder.resolve("second.log"); + + Process first = startCacheLockProcess(firstVersion, firstAcquired, firstRelease, firstLog); + Process second = null; + try { + assertTrue(waitForFile(firstAcquired, PROCESS_TIMEOUT)); + + second = startCacheLockProcess(secondVersion, secondAcquired, secondRelease, secondLog); + assertTrue(waitForFile(secondAcquired, PROCESS_TIMEOUT), + "A different dumper version was blocked by an unrelated cache lock"); + + Files.createFile(firstRelease); + Files.createFile(secondRelease); + waitForProcess(first, firstLog); + waitForProcess(second, secondLog); + } finally { + releaseProcess(firstRelease); + releaseProcess(secondRelease); + stopProcess(first); + stopProcess(second); + } + + assertFalse(ClangResources.getCacheLockFolder(firstVersion).exists()); + assertFalse(ClangResources.getCacheLockFolder(secondVersion).exists()); + } + + @Test + public void cacheLockRecoversAfterOwningJvmIsTerminated() throws Exception { + var versionFolder = Files.createDirectory(tempFolder.resolve("version")).toFile(); + var firstAcquired = tempFolder.resolve("first.acquired"); + var firstRelease = tempFolder.resolve("first.release"); + var secondAcquired = tempFolder.resolve("second.acquired"); + var secondRelease = tempFolder.resolve("second.release"); + var firstLog = tempFolder.resolve("first.log"); + var secondLog = tempFolder.resolve("second.log"); + + Process first = startCacheLockProcess(versionFolder, firstAcquired, firstRelease, firstLog); + Process second = null; + try { + assertTrue(waitForFile(firstAcquired, PROCESS_TIMEOUT)); + first.destroyForcibly(); + assertTrue(first.waitFor(PROCESS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)); + + second = startCacheLockProcess(versionFolder, secondAcquired, secondRelease, secondLog); + assertTrue(waitForFile(secondAcquired, PROCESS_TIMEOUT), + "A cache lock left by a terminated JVM was not recovered"); + + Files.createFile(secondRelease); + waitForProcess(second, secondLog); + } finally { + releaseProcess(firstRelease); + releaseProcess(secondRelease); + stopProcess(first); + stopProcess(second); + } + + assertFalse(ClangResources.getCacheLockFolder(versionFolder).exists()); + } + + @Test + public void cacheLockDoesNotTreatLiveOwnerAsStaleWhenDirectoryIsOld() throws Exception { + var versionFolder = Files.createDirectory(tempFolder.resolve("version")).toFile(); + var lockFolder = ClangResources.getCacheLockFolder(versionFolder).toPath(); + var secondAcquired = tempFolder.resolve("second.acquired"); + var secondRelease = tempFolder.resolve("second.release"); + var secondLog = tempFolder.resolve("second.log"); + + ClangResources.CacheLock first = ClangResources.acquireCacheLock(versionFolder); + Process second = null; + try { + Files.setLastModifiedTime(lockFolder, FileTime.from(Instant.now().minus(Duration.ofHours(1)))); + + second = startCacheLockProcess(versionFolder, secondAcquired, secondRelease, secondLog); + assertFalse(waitForFile(secondAcquired, Duration.ofMillis(750)), + "A live cache-lock owner was incorrectly treated as stale"); + + first.close(); + first = null; + assertTrue(waitForFile(secondAcquired, PROCESS_TIMEOUT)); + Files.createFile(secondRelease); + waitForProcess(second, secondLog); + } finally { + if (first != null) { + first.close(); + } + releaseProcess(secondRelease); + stopProcess(second); + } + + assertFalse(lockFolder.toFile().exists()); + } + + @Test + public void cacheLockUsesProcessStartTimeWhenPidIsStillAlive() throws Exception { + var versionFolder = Files.createDirectory(tempFolder.resolve("version")).toFile(); + var lockFolder = ClangResources.getCacheLockFolder(versionFolder).toPath(); + Files.createDirectory(lockFolder); + assumeTrue(ProcessHandle.current().info().startInstant().isPresent(), + "The current platform does not expose process start times"); + Files.writeString(lockFolder.resolve("owner"), + ProcessHandle.current().pid() + System.lineSeparator() + Instant.EPOCH + System.lineSeparator()); + + var acquired = tempFolder.resolve("acquired"); + var release = tempFolder.resolve("release"); + var log = tempFolder.resolve("child.log"); + Process child = startCacheLockProcess(versionFolder, acquired, release, log); + try { + assertTrue(waitForFile(acquired, PROCESS_TIMEOUT), + "A lock with a reused PID and a different process start time was not recovered"); + Files.createFile(release); + waitForProcess(child, log); + } finally { + releaseProcess(release); + stopProcess(child); + } + + assertFalse(lockFolder.toFile().exists()); + } + + @Test + public void cacheLockRecoversAnOldOwnerlessLock() throws Exception { + var versionFolder = Files.createDirectory(tempFolder.resolve("version")).toFile(); + var lockFolder = ClangResources.getCacheLockFolder(versionFolder).toPath(); + Files.createDirectory(lockFolder); + Files.setLastModifiedTime(lockFolder, FileTime.from(Instant.now().minus(Duration.ofHours(1)))); + + var acquired = tempFolder.resolve("acquired"); + var release = tempFolder.resolve("release"); + var log = tempFolder.resolve("child.log"); + Process child = startCacheLockProcess(versionFolder, acquired, release, log); + try { + assertTrue(waitForFile(acquired, PROCESS_TIMEOUT), "An old ownerless cache lock was not recovered"); + Files.createFile(release); + waitForProcess(child, log); + } finally { + releaseProcess(release); + stopProcess(child); + } + + assertFalse(lockFolder.toFile().exists()); + } + + @Test + public void cacheLockDoesNotStealARecentOwnerlessLock() throws Exception { + var versionFolder = Files.createDirectory(tempFolder.resolve("version")).toFile(); + var lockFolder = ClangResources.getCacheLockFolder(versionFolder).toPath(); + Files.createDirectory(lockFolder); + + var acquired = tempFolder.resolve("acquired"); + var release = tempFolder.resolve("release"); + var log = tempFolder.resolve("child.log"); + Process child = startCacheLockProcess(versionFolder, acquired, release, log); + try { + assertFalse(waitForFile(acquired, Duration.ofMillis(750)), + "A lock without owner metadata was stolen before its claim was old enough"); + + Files.delete(lockFolder); + assertTrue(waitForFile(acquired, PROCESS_TIMEOUT)); + Files.createFile(release); + waitForProcess(child, log); + } finally { + releaseProcess(release); + stopProcess(child); + } + + assertFalse(lockFolder.toFile().exists()); + } + + @Test + public void staleCacheCleanupSkipsLockedVersionAcrossJvms() throws Exception { + var currentVersion = Files.createDirectory(tempFolder.resolve("current")).toFile(); + var staleVersion = Files.createDirectory(tempFolder.resolve("stale")).toFile(); + Files.writeString(staleVersion.toPath().resolve("last-used.txt"), + Instant.now().minus(Duration.ofDays(61)).toString()); + + var acquired = tempFolder.resolve("acquired"); + var release = tempFolder.resolve("release"); + var log = tempFolder.resolve("child.log"); + Process child = startCacheLockProcess(staleVersion, acquired, release, log); + try { + assertTrue(waitForFile(acquired, PROCESS_TIMEOUT)); + + var parser = CodeParser.newInstance(); + parser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); + new ClangResources(parser).deleteStaleVersions(Instant.now(), currentVersion); + assertTrue(staleVersion.isDirectory(), "Stale cleanup deleted a version locked by another JVM"); + + Files.createFile(release); + waitForProcess(child, log); + } finally { + releaseProcess(release); + stopProcess(child); + } + + var parser = CodeParser.newInstance(); + parser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); + new ClangResources(parser).deleteStaleVersions(Instant.now(), currentVersion); + assertFalse(staleVersion.exists()); + } + + @Test + public void releaseResourcesCanBeInitializedBySeparateJvms() throws Exception { + var cacheFolder = Files.createDirectory(tempFolder.resolve("cache")).toFile(); + var firstDone = tempFolder.resolve("first.done"); + var secondDone = tempFolder.resolve("second.done"); + var firstLog = tempFolder.resolve("first.log"); + var secondLog = tempFolder.resolve("second.log"); + + Process first = startResourceProcess(cacheFolder, firstDone, firstLog); + Process second = startResourceProcess(cacheFolder, secondDone, secondLog); + try { + waitForProcess(first, firstLog); + waitForProcess(second, secondLog); + } finally { + stopProcess(first); + stopProcess(second); + } + + var firstExecutable = new File(Files.readString(firstDone).trim()); + var secondExecutable = new File(Files.readString(secondDone).trim()); + assertEquals(firstExecutable.getAbsoluteFile(), secondExecutable.getAbsoluteFile()); + assertTrue(firstExecutable.isFile()); + assertFalse(cacheFolder.toPath().resolve(ClangAstWebResource.getReleaseTag() + ".cache.lock").toFile().exists()); + } + + @Test + public void cacheLockReleaseDoesNotRemoveAReclaimedLock() throws Exception { + var versionFolder = Files.createDirectory(tempFolder.resolve("version")).toFile(); + var lockFolder = ClangResources.getCacheLockFolder(versionFolder).toPath(); + ClangResources.CacheLock first = ClangResources.acquireCacheLock(versionFolder); + + Files.writeString(first.ownerFile().toPath(), Long.MAX_VALUE + System.lineSeparator()); + ClangResources.CacheLock second = ClangResources.acquireCacheLock(versionFolder); + + try { + first.close(); + assertTrue(Files.isDirectory(lockFolder), + "A stale lock owner released and deleted a replacement owner's lock"); + } finally { + second.close(); + } + } + + @Test + public void cachedUseDoesNotBypassTheCacheLockBeforeUpdatingLastUsed() throws Exception { + var parser = CodeParser.newInstance(); + parser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); + var resources = new ClangResources(parser); + var versionFolder = resources.getClangResourceFolder(); + var fakeExecutable = Files.createFile(tempFolder.resolve("fake-tool")).toFile(); + var cache = getClangFilesCache(); + var cacheKey = getClangFilesCacheKey(resources, LibcMode.SYSTEM); + var cachedFiles = new ClangFiles(fakeExecutable, List.of()); + cache.put(cacheKey, cachedFiles); + + ClangResources.CacheLock lock = ClangResources.acquireCacheLock(versionFolder); + var executor = Executors.newSingleThreadExecutor(); + try { + var future = executor.submit(() -> resources.getClangFiles(LibcMode.SYSTEM)); + assertThrows(TimeoutException.class, () -> future.get(750, TimeUnit.MILLISECONDS), + "A cached use updated last-used without coordinating with the cache lock"); + + lock.close(); + lock = null; + assertSame(cachedFiles, future.get(PROCESS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)); + } finally { + if (lock != null) { + lock.close(); + } + executor.shutdownNow(); + executor.awaitTermination(PROCESS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + cache.remove(cacheKey); + } + } + + @Test + public void sameJvmInstancesShareReleaseCacheInitialization() throws Exception { + var firstParser = CodeParser.newInstance(); + firstParser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); + firstParser.set(CodeParser.CUDA_PATH, CodeParser.getBuiltinOption()); + + var secondParser = CodeParser.newInstance(); + secondParser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); + secondParser.set(CodeParser.CUDA_PATH, CodeParser.getBuiltinOption()); + + var thirdParser = CodeParser.newInstance(); + thirdParser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); + thirdParser.set(CodeParser.CUDA_PATH, CodeParser.getBuiltinOption()); + + var executor = Executors.newFixedThreadPool(3); + try { + var first = executor.submit(() -> new ClangResources(firstParser).getClangFiles(LibcMode.SYSTEM)); + var second = executor.submit(() -> new ClangResources(secondParser).getClangFiles(LibcMode.SYSTEM)); + var third = executor.submit( + () -> new ClangResources(thirdParser).getClangFiles(LibcMode.BUILTIN_AND_LIBC)); + + var firstFiles = first.get(PROCESS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + var secondFiles = second.get(PROCESS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + var thirdFiles = third.get(PROCESS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + + assertSame(firstFiles, secondFiles, "Same-JVM instances did not share the ClangFiles cache entry"); + assertEquals(firstFiles.clangExecutable().getAbsoluteFile(), thirdFiles.clangExecutable().getAbsoluteFile()); + assertTrue(firstFiles.clangExecutable().isFile()); + } finally { + executor.shutdownNow(); + executor.awaitTermination(PROCESS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } + } + + private Process startCacheLockProcess(File versionFolder, Path acquired, Path release, Path log) + throws IOException { + + var javaExecutable = Path.of(System.getProperty("java.home"), "bin", + SupportedPlatform.getCurrentPlatform().isWindows() ? "java.exe" : "java"); + + return new ProcessBuilder( + javaExecutable.toString(), + "-cp", + System.getProperty("java.class.path"), + CacheLockProcess.class.getName(), + versionFolder.getAbsolutePath(), + acquired.toAbsolutePath().toString(), + release.toAbsolutePath().toString()) + .redirectErrorStream(true) + .redirectOutput(log.toFile()) + .start(); + } + + private Process startResourceProcess(File cacheFolder, Path done, Path log) throws IOException { + var javaExecutable = Path.of(System.getProperty("java.home"), "bin", + SupportedPlatform.getCurrentPlatform().isWindows() ? "java.exe" : "java"); + + return new ProcessBuilder( + javaExecutable.toString(), + "-cp", + System.getProperty("java.class.path"), + CacheLockProcess.class.getName(), + "resources", + cacheFolder.getAbsolutePath(), + done.toAbsolutePath().toString()) + .redirectErrorStream(true) + .redirectOutput(log.toFile()) + .start(); + } + + @SuppressWarnings("unchecked") + private Map getClangFilesCache() throws ReflectiveOperationException { + var cacheField = ClangResources.class.getDeclaredField("CLANG_FILES_CACHE"); + cacheField.setAccessible(true); + return (Map) cacheField.get(null); + } + + private String getClangFilesCacheKey(ClangResources resources, LibcMode libcMode) { + var source = ClangAstWebResource.getDumperSource(); + var sourceKey = source instanceof Release + ? source + "_" + resources.getClangResourceFolder().getAbsolutePath() + : source.toString(); + return libcMode.name() + "_false_" + sourceKey; + } + + private boolean waitForFile(Path file, Duration timeout) throws InterruptedException { + var deadline = System.nanoTime() + timeout.toNanos(); + do { + if (Files.isRegularFile(file)) { + return true; + } + + Thread.sleep(10); + } while (System.nanoTime() < deadline); + + return Files.isRegularFile(file); + } + + private void waitForProcess(Process process, Path log) throws Exception { + assertTrue(process.waitFor(PROCESS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS), + () -> "Child JVM did not finish. Output: " + readLog(log)); + assertEquals(0, process.exitValue(), () -> "Child JVM failed. Output: " + readLog(log)); + } + + private String readLog(Path log) { + try { + return Files.readString(log); + } catch (IOException e) { + return ""; + } + } + + private void releaseProcess(Path release) throws IOException { + if (release != null && !Files.exists(release)) { + Files.createFile(release); + } + } + + private void stopProcess(Process process) throws InterruptedException { + if (process == null || !process.isAlive()) { + return; + } + + process.destroyForcibly(); + process.waitFor(PROCESS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } + + public static final class CacheLockProcess { + + private CacheLockProcess() { + } + + public static void main(String[] args) throws Exception { + if (args[0].equals("resources")) { + var parser = CodeParser.newInstance(); + parser.set(CodeParser.DUMPER_FOLDER, new File(args[1])); + var clangFiles = new ClangResources(parser).getClangFiles(LibcMode.SYSTEM); + Files.writeString(Path.of(args[2]), clangFiles.clangExecutable().getAbsolutePath()); + return; + } + + var versionFolder = new File(args[0]); + var acquired = Path.of(args[1]); + var release = Path.of(args[2]); + + try (var ignored = ClangResources.acquireCacheLock(versionFolder)) { + Files.writeString(acquired, Long.toString(ProcessHandle.current().pid())); + while (!Files.exists(release)) { + Thread.sleep(10); + } + } + } + } + + @Test + public void builtinCudaIncludesAreAvailableWithSystemLibc() { + var parser = CodeParser.newInstance(); + parser.set(CodeParser.CUDA_PATH, CodeParser.getBuiltinOption()); + + var clangFiles = new ClangResources(parser).getClangFiles(LibcMode.SYSTEM); + var hasCudaWrapper = clangFiles.builtinIncludes().stream() + .map(folder -> new File(folder, "__clang_cuda_runtime_wrapper.h")) + .anyMatch(File::isFile); + + assertTrue(hasCudaWrapper, "Built-in CUDA must provide Clang's CUDA runtime wrapper independently of libc"); + } + + @Test + public void builtinCudaArchiveHasCanonicalInstallationLayout() { + var parser = CodeParser.newInstance(); + parser.set(CodeParser.DUMPER_FOLDER, tempFolder.toFile()); + parser.set(CodeParser.CUDA_PATH, CodeParser.getBuiltinOption()); + + var cudaFolder = new ClangResources(parser).getBuiltinCudaLib(); + + assertEquals(tempFolder.resolve("cuda/cudalib").toFile().getAbsolutePath(), + cudaFolder.getAbsolutePath()); + assertTrue(new File(cudaFolder, "include/cuda.h").isFile()); + assertTrue(new File(cudaFolder, "include/cuda_runtime.h").isFile()); + assertTrue(new File(cudaFolder, "nvvm/libdevice/libdevice.10.bc").isFile()); + } + + @Test + public void libcDetectionIsScopedToTheExecutable() throws IOException { + assumeTrue(!SupportedPlatform.getCurrentPlatform().isWindows(), "Shell fixtures require a Unix executable"); + + var systemLibcDumper = tempFolder.resolve("system-libc-dumper"); + Files.writeString(systemLibcDumper, + "#!/bin/sh\nprintf '%s\\n' '" + TopLevelNodesParser.getTopLevelNodesHeader() + "'\n"); + assertTrue(systemLibcDumper.toFile().setExecutable(true)); + + var builtinLibcDumper = tempFolder.resolve("builtin-libc-dumper"); + Files.writeString(builtinLibcDumper, "#!/bin/sh\nexit 1\n"); + assertTrue(builtinLibcDumper.toFile().setExecutable(true)); + + assertFalse(ClangResources.useBuiltinLibc(systemLibcDumper.toFile(), LibcMode.AUTO)); + assertTrue(ClangResources.useBuiltinLibc(builtinLibcDumper.toFile(), LibcMode.AUTO)); + } +} diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/CxxCudaTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/CxxCudaTest.java index 1a6b37d3f..2a857db62 100644 --- a/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/CxxCudaTest.java +++ b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/CxxCudaTest.java @@ -15,21 +15,25 @@ import org.junit.jupiter.api.Test; +import pt.up.fe.specs.clang.ClangAstKeys; +import pt.up.fe.specs.clang.LibcMode; import pt.up.fe.specs.clang.parser.CxxCudaTester; -/** - * Disabled tests, they are failing in the CI server. Even when passing the --cuda-path built-in library, the parser - * fails to find the CUDA library. - * - * @author JBispo - * - */ +/** Verifies built-in CUDA parsing through Clava's historical cudalib archive. */ public class CxxCudaTest { @Test public void testAtomicAdd() { new CxxCudaTester("atomicAdd.cu").test(); } + @Test + public void testAtomicAddWithSystemLibc() { + new CxxCudaTester("atomicAdd.cu") + .set(ClangAstKeys.LIBC_CXX_MODE, LibcMode.SYSTEM) + .onePass() + .test(); + } + @Test public void testConvolutionCache() { new CxxCudaTester("convolution_cache.cu").test(); diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/CxxTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/CxxTest.java index b8cdfe572..7de8c9042 100644 --- a/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/CxxTest.java +++ b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/CxxTest.java @@ -287,6 +287,13 @@ public void testStrings() { new CxxTester("strings.cpp").test(); } + @Test + public void testUnevaluatedStrings() { + new CxxTester("unevaluated_strings.cpp") + .addFlags("-std=c++26") + .test(); + } + // -Xclang-ast-dump-nostdinc-nocudalib-nocudainc--cuda-gpu-arch=sm_30 // "--cuda-device-only" diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/InitializationStyleTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/InitializationStyleTest.java new file mode 100644 index 000000000..5e1f99532 --- /dev/null +++ b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/InitializationStyleTest.java @@ -0,0 +1,58 @@ +/** + * Copyright 2026 SPeCS. + *

    + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

    + * http://www.apache.org/licenses/LICENSE-2.0 + *

    + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package pt.up.fe.specs.clang.parser.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import pt.up.fe.specs.clang.codeparser.CodeParser; +import pt.up.fe.specs.clava.ast.decl.VarDecl; +import pt.up.fe.specs.clava.ast.decl.enums.InitializationStyle; +import pt.up.fe.specs.clava.ast.extra.App; +import pt.up.fe.specs.util.SpecsIo; +import pt.up.fe.specs.util.SpecsSystem; + +public class InitializationStyleTest { + + @TempDir + Path tempFolder; + + @Test + public void parenthesizedListInitializationKeepsAllArguments() { + SpecsSystem.programStandardInit(); + + File sourceFile = SpecsIo.resourceCopy("cxx/paren_list_initialization.cpp", tempFolder.toFile(), false, true); + App app = CodeParser.newInstance().parse(List.of(sourceFile), List.of("-std=c++20")); + + VarDecl point = app.getDescendants(VarDecl.class).stream() + .filter(varDecl -> varDecl.getDeclName().equals("point")) + .findFirst() + .orElseThrow(); + + assertEquals(InitializationStyle.ParenListInit, point.get(VarDecl.INIT_STYLE)); + assertEquals("Point point(1, 2)", point.getCode()); + } + + @Test + public void javascriptInitializationStyleNamesRemainCompatible() { + assertEquals("callinit", InitializationStyle.CALL_INIT.getString()); + assertEquals("listinit", InitializationStyle.LIST_INIT.getString()); + } +} diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/SourceLocationsTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/SourceLocationsTest.java new file mode 100644 index 000000000..c9e24c6c8 --- /dev/null +++ b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/SourceLocationsTest.java @@ -0,0 +1,88 @@ +/** + * Copyright 2026 SPeCS. + *

    + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

    + * http://www.apache.org/licenses/LICENSE-2.0 + *

    + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package pt.up.fe.specs.clang.parser.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import pt.up.fe.specs.clang.codeparser.CodeParser; +import pt.up.fe.specs.clava.ClavaNode; +import pt.up.fe.specs.clava.SourceRange; +import pt.up.fe.specs.clava.ast.decl.TemplateTypeParmDecl; +import pt.up.fe.specs.clava.ast.decl.VarDecl; +import pt.up.fe.specs.clava.ast.extra.App; +import pt.up.fe.specs.util.SpecsIo; +import pt.up.fe.specs.util.SpecsSystem; + +public class SourceLocationsTest { + + private static final String RESOURCE = "cxx/source_locations.cpp"; + + @TempDir + Path tempFolder; + + @Test + public void sourceLocationsUseRealFileCoordinates() { + SpecsSystem.programStandardInit(); + + File sourceFile = SpecsIo.resourceCopy(RESOURCE, tempFolder.toFile(), false, true); + App app = CodeParser.newInstance().parse(List.of(sourceFile), List.of("-std=c++11")); + + VarDecl ordinary = find(app.getDescendants(VarDecl.class), varDecl -> varDecl.getDeclName().equals("ordinary")); + assertRange(ordinary, false, 7, 1, 7, 16); + + VarDecl macro = find(app.getDescendants(VarDecl.class), varDecl -> varDecl.getDeclName().equals("macro_value")); + assertRange(macro, true, 6, 1, 6, 24); + assertRange(macro.getInit().orElseThrow(), true, 6, 1, 6, 24); + + VarDecl pastedReference = find(app.getDescendants(VarDecl.class), + varDecl -> varDecl.getDeclName().equals("pasted_reference")); + assertRange(pastedReference.getInit().orElseThrow(), true, 9, 24, 9, 36); + + TemplateTypeParmDecl templateParameter = find(app.getDescendantsAndFields(TemplateTypeParmDecl.class), + parameter -> parameter.getDeclName().equals("BinaryType")); + assertRange(templateParameter, false, 16, 11, 16, 54); + } + + private static T find(List nodes, Predicate predicate) { + return nodes.stream() + .filter(predicate) + .findFirst() + .orElseThrow(); + } + + private static void assertRange(ClavaNode node, boolean isMacro, int startLine, int startColumn, int endLine, + int endColumn) { + + SourceRange location = node.getLocation(); + + assertEquals("source_locations.cpp", location.getFilename()); + assertEquals("source_locations.cpp", Path.of(location.getStartFilepath()).getFileName().toString()); + assertEquals("source_locations.cpp", Path.of(location.getEndFilepath()).getFileName().toString()); + assertEquals(startLine, location.getStartLine()); + assertEquals(startColumn, location.getStartCol()); + assertEquals(endLine, location.getEndLine()); + assertEquals(endColumn, location.getEndCol()); + assertEquals(isMacro, node.get(ClavaNode.IS_MACRO)); + assertFalse(location.toString().contains("")); + } +} diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/UnaryTransformTypeTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/UnaryTransformTypeTest.java new file mode 100644 index 000000000..27d54bd00 --- /dev/null +++ b/ClangAstParser/test/pt/up/fe/specs/clang/parser/tests/UnaryTransformTypeTest.java @@ -0,0 +1,85 @@ +/** + * Copyright 2026 SPeCS. + *

    + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

    + * http://www.apache.org/licenses/LICENSE-2.0 + *

    + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package pt.up.fe.specs.clang.parser.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import pt.up.fe.specs.clang.codeparser.CodeParser; +import pt.up.fe.specs.clava.ast.extra.App; +import pt.up.fe.specs.clava.ast.type.Type; +import pt.up.fe.specs.clava.ast.type.UnaryTransformType; +import pt.up.fe.specs.clava.ast.type.enums.UnaryTransformTypeKind; +import pt.up.fe.specs.util.SpecsSystem; + +public class UnaryTransformTypeTest { + + private static final String SOURCE = """ + template + struct Dependent { + using type = __underlying_type(T); + }; + + enum Resolved { Value }; + using ResolvedType = __underlying_type(Resolved); + """; + + @TempDir + Path tempFolder; + + @Test + public void dependentTransformsMayHaveNoUnderlyingType() throws IOException { + SpecsSystem.programStandardInit(); + + File sourceFile = tempFolder.resolve("unary_transform_type.cpp").toFile(); + Files.writeString(sourceFile.toPath(), SOURCE); + + App app = CodeParser.newInstance().parse(List.of(sourceFile), List.of("-std=c++11")); + + List transforms = app.getDescendantsAndFields(UnaryTransformType.class).stream() + .filter(transform -> transform.get(UnaryTransformType.KIND) == UnaryTransformTypeKind.EnumUnderlyingType) + .toList(); + + assertEquals(2, transforms.size()); + + UnaryTransformType dependentTransform = transforms.stream() + .filter(transform -> transform.getUnderlyingType().isEmpty()) + .findFirst() + .orElseThrow(); + + assertNotNull(dependentTransform.getBaseType()); + assertEquals(1, dependentTransform.getNodeFields().size()); + assertTrue(dependentTransform.getNodeFields().contains(dependentTransform.getBaseType())); + + UnaryTransformType resolvedTransform = transforms.stream() + .filter(transform -> transform.getUnderlyingType().isPresent()) + .findFirst() + .orElseThrow(); + + Type resolvedUnderlyingType = resolvedTransform.getUnderlyingType().orElseThrow(); + assertNotNull(resolvedTransform.getBaseType()); + assertTrue(resolvedTransform.getNodeFields().contains(resolvedTransform.getBaseType())); + assertTrue(resolvedTransform.getNodeFields().contains(resolvedUnderlyingType)); + } +} diff --git a/ClangAstParser/test/pt/up/fe/specs/clang/parsers/ClavaNodesTest.java b/ClangAstParser/test/pt/up/fe/specs/clang/parsers/ClavaNodesTest.java new file mode 100644 index 000000000..f4a42736a --- /dev/null +++ b/ClangAstParser/test/pt/up/fe/specs/clang/parsers/ClavaNodesTest.java @@ -0,0 +1,71 @@ +/** + * Copyright 2026 SPeCS. + *

    + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

    + * http://www.apache.org/licenses/LICENSE-2.0 + *

    + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package pt.up.fe.specs.clang.parsers; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.suikasoft.jOptions.Interfaces.DataStore; + +import pt.up.fe.specs.clava.ast.expr.Expr; +import pt.up.fe.specs.clava.context.ClavaContext; + +public class ClavaNodesTest { + + @Test + public void optionalNodeResolvesPresentNode() { + var context = new ClavaContext(); + var factory = context.getFactory(); + var clavaNodes = new ClavaNodes(factory); + DataStore data = factory.newDataStore(Expr.class); + var type = factory.nullType(); + + clavaNodes.getNodes().put("type-id", type); + clavaNodes.queueSetOptionalNode(data, Expr.TYPE, "type-id"); + clavaNodes.getQueuedActions().forEach(Runnable::run); + + assertSame(type, data.get(Expr.TYPE).orElseThrow()); + } + + @Test + public void optionalNodeUsesEmptyForExplicitNullId() { + var context = new ClavaContext(); + var factory = context.getFactory(); + var clavaNodes = new ClavaNodes(factory); + DataStore data = factory.newDataStore(Expr.class); + + clavaNodes.queueSetOptionalNode(data, Expr.TYPE, "nullptr_type"); + clavaNodes.getQueuedActions().forEach(Runnable::run); + + assertTrue(data.get(Expr.TYPE).isEmpty()); + } + + @Test + public void optionalNodeRejectsUnresolvedNodeId() { + var context = new ClavaContext(); + var factory = context.getFactory(); + var clavaNodes = new ClavaNodes(factory); + DataStore data = factory.newDataStore(Expr.class); + + clavaNodes.queueSetOptionalNode(data, Expr.TYPE, "missing-id"); + + var exception = assertThrows(NullPointerException.class, + () -> clavaNodes.getQueuedActions().forEach(Runnable::run)); + + assertTrue(exception.getMessage().contains("Could not resolve optional node 'missing-id'")); + assertTrue(exception.getMessage().contains("key 'type'")); + } +} diff --git a/Clava-JS/api/Joinpoints.ts b/Clava-JS/api/Joinpoints.ts index 659971877..98997a143 100644 --- a/Clava-JS/api/Joinpoints.ts +++ b/Clava-JS/api/Joinpoints.ts @@ -2811,12 +2811,12 @@ export class TypedefType extends Type { * This and the "type" declaration below. */ export const StorageClass = { - NONE: "none", - AUTO: "auto", - EXTERN: "extern", - PRIVATE_EXTERN: "private_extern", - REGISTER: "register", - STATIC: "static", + NONE: "NONE", + AUTO: "AUTO", + EXTERN: "EXTERN", + PRIVATE_EXTERN: "PRIVATE_EXTERN", + REGISTER: "REGISTER", + STATIC: "STATIC", } as const; export type StorageClass = typeof StorageClass[keyof typeof StorageClass]; @@ -2826,12 +2826,12 @@ export type StorageClass = typeof StorageClass[keyof typeof StorageClass]; * This and the "type" declaration below. */ export const Relation = { - LE: "le", - LT: "lt", - GE: "ge", - GT: "gt", - EQ: "eq", - NE: "ne", + LE: "LE", + LT: "LT", + GE: "GE", + GT: "GT", + EQ: "EQ", + NE: "NE", } as const; export type Relation = typeof Relation[keyof typeof Relation]; diff --git a/Clava-JS/code/sideEffects.ts b/Clava-JS/code/sideEffects.ts index 59d7bf76f..2c62d3bf4 100644 --- a/Clava-JS/code/sideEffects.ts +++ b/Clava-JS/code/sideEffects.ts @@ -18,14 +18,14 @@ const datastore = Weaver.getWeaverEngine().getData().get(); datastore.set(CxxWeaverOptions.DISABLE_CLAVA_INFO, true); datastore.set( CodeParser.DUMPER_FOLDER, - new JavaTypes.File(getVersionedCacheDir()) + new JavaTypes.File(getClangDumperCacheDir()) ); /** Code to obtain temporary folder **/ -function getVersionedCacheDir(): string { - // Use name+version to isolate different installed versions - return path.join(getCacheBaseDir(), pkg.name, pkg.version); +function getClangDumperCacheDir(): string { + // The version will be added by the installer to isolate different installed versions + return path.join(getCacheBaseDir(), pkg.name, "clang-dumper"); } function getCacheBaseDir(): string { diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/attr/enums/AttributeKind.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/attr/enums/AttributeKind.java index 8158847f4..5b0e3d7e7 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/attr/enums/AttributeKind.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/attr/enums/AttributeKind.java @@ -24,12 +24,19 @@ import pt.up.fe.specs.util.utilities.CachedItems; public enum AttributeKind implements StringProvider { - AddressSpace, AnnotateType, + ArmIn, + ArmInOut, ArmMveStrictPolymorphism, + ArmOut, + ArmPreserves, + ArmStreaming, + ArmStreamingCompatible, BTFTypeTag, CmseNSCall, + HLSLGroupSharedAddressSpace, + HLSLParamModifier, NoDeref, ObjCGC, ObjCInertUnsafeUnretained, @@ -49,15 +56,17 @@ public enum AttributeKind implements StringProvider { TypeNullable, TypeNullableResult, UPtr, + WebAssemblyFuncref, + CodeAlign, FallThrough, Likely, MustTail, OpenCLUnrollHint, - Suppress, Unlikely, AlwaysInline, NoInline, NoMerge, + Suppress, AArch64SVEPcs, AArch64VectorPcs, AMDGPUKernelCall, @@ -67,6 +76,7 @@ public enum AttributeKind implements StringProvider { FastCall, IntelOclBicc, LifetimeBound, + M68kRTD, MSABI, NSReturnsRetained, ObjCOwnership, @@ -94,6 +104,8 @@ public enum AttributeKind implements StringProvider { PassObjectSize, ReleaseHandle, UseHandle, + HLSLSV_DispatchThreadID, + HLSLSV_GroupIndex, AMDGPUFlatWorkGroupSize, AMDGPUNumSGPR, AMDGPUNumVGPR, @@ -116,6 +128,8 @@ public enum AttributeKind implements StringProvider { ArcWeakrefUnavailable, ArgumentWithTypeTag, ArmBuiltinAlias, + ArmLocallyStreaming, + ArmNew, Artificial, AsmLabel, AssertCapability, @@ -124,7 +138,9 @@ public enum AttributeKind implements StringProvider { AssumeAligned, Assumption, Availability, + AvailableOnlyInDefaultEvalMethod, BPFPreserveAccessIndex, + BPFPreserveStaticOffset, BTFDeclTag, Blocks, Builtin, @@ -153,6 +169,7 @@ public enum AttributeKind implements StringProvider { CapturedRecord, Cleanup, CmseNSEntry, + CodeModel, CodeSeg, Cold, Common, @@ -163,6 +180,12 @@ public enum AttributeKind implements StringProvider { ConsumableAutoCast, ConsumableSetOnRead, Convergent, + CoroDisableLifetimeBound, + CoroLifetimeBound, + CoroOnlyDestroyWhenComplete, + CoroReturnType, + CoroWrapper, + CountedBy, DLLExport, DLLExportStaticLocal, DLLImport, @@ -193,7 +216,8 @@ public enum AttributeKind implements StringProvider { GuardedVar, HIPManaged, HLSLNumThreads, - HLSLSV_GroupIndex, + HLSLResource, + HLSLResourceBinding, HLSLShader, Hot, IBAction, @@ -209,6 +233,7 @@ public enum AttributeKind implements StringProvider { M68kInterrupt, MIGServerRoutine, MSAllocator, + MSConstexpr, MSInheritance, MSNoVTable, MSP430Interrupt, @@ -216,6 +241,7 @@ public enum AttributeKind implements StringProvider { MSVtorDisp, MaxFieldAlignment, MayAlias, + MaybeUndef, MicroMips, MinSize, MinVectorWidth, @@ -227,6 +253,7 @@ public enum AttributeKind implements StringProvider { NSErrorDomain, NSReturnsAutoreleased, NSReturnsNotRetained, + NVPTXKernel, Naked, NoAlias, NoCommon, @@ -246,6 +273,7 @@ public enum AttributeKind implements StringProvider { NoThreadSafetyAnalysis, NoThrow, NoUniqueAddress, + NoUwtable, NotTailCalled, OMPAllocateDecl, OMPCaptureNoInit, @@ -288,11 +316,13 @@ public enum AttributeKind implements StringProvider { PragmaClangRodataSection, PragmaClangTextSection, PreferredName, + PreferredType, PtGuardedBy, PtGuardedVar, Pure, RISCVInterrupt, RandomizeLayout, + ReadOnlyPlacement, Reinitializes, ReleaseCapability, ReqdWorkGroupSize, @@ -313,6 +343,7 @@ public enum AttributeKind implements StringProvider { SpeculativeLoadHardening, StandaloneDebug, StrictFP, + StrictGuardStackCheck, SwiftAsync, SwiftAsyncError, SwiftAsyncName, @@ -320,12 +351,15 @@ public enum AttributeKind implements StringProvider { SwiftBridge, SwiftBridgedTypedef, SwiftError, + SwiftImportAsNonGeneric, + SwiftImportPropertyAsAccessors, SwiftName, SwiftNewType, SwiftPrivate, TLSModel, Target, TargetClones, + TargetVersion, TestTypestate, TransparentUnion, TrivialABI, @@ -334,6 +368,7 @@ public enum AttributeKind implements StringProvider { TypeVisibility, Unavailable, Uninitialized, + UnsafeBufferUsage, Unused, Used, UsingIfExists, @@ -382,6 +417,8 @@ public enum AttributeKind implements StringProvider { Overloadable, RenderScriptKernel, SwiftObjCMembers, + SwiftVersionedAddition, + SwiftVersionedRemoval, Thread, FirstAttr, LastAttr, @@ -398,7 +435,9 @@ public enum AttributeKind implements StringProvider { FirstInheritableParamAttr, LastInheritableParamAttr, FirstParameterABIAttr, - LastParameterABIAttr; + LastParameterABIAttr, + FirstHLSLAnnotationAttr, + LastHLSLAnnotationAttr; private static final Lazy> ENUM_HELPER = EnumHelperWithValue .newLazyHelperWithValue(AttributeKind.class); diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/decl/enums/InitializationStyle.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/decl/enums/InitializationStyle.java index 21e3022c4..45a8b01f6 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/decl/enums/InitializationStyle.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/decl/enums/InitializationStyle.java @@ -24,13 +24,15 @@ import pt.up.fe.specs.util.providers.StringProvider; public enum InitializationStyle implements StringProvider { - NO_INIT, CINIT, // C-style initialization with assignment + // Keep these explicit names for compatibility: build-interfaces exposes strings to TS, with no TS enum to convert + // them to. CALL_INIT("callinit"), // Call-style initialization (C++98) - LIST_INIT("listinit"); // Direct list-initialization (C++11) + LIST_INIT("listinit"), // Direct list-initialization (C++11) + ParenListInit; private static Lazy> ENUM_HELPER = EnumHelperWithValue - .newLazyHelperWithValue(InitializationStyle.class, NO_INIT); + .newLazyHelperWithValue(InitializationStyle.class); public static EnumHelperWithValue getHelper() { return ENUM_HELPER.get(); @@ -56,10 +58,10 @@ public String getCode(VarDecl node) { switch (this) { case CINIT: return cinitCode(node); - case NO_INIT: - return ""; case CALL_INIT: return callInitCode(node); + case ParenListInit: + return parenListInitCode(node); case LIST_INIT: return listInitCode(node); default: @@ -98,6 +100,16 @@ private String callInitCode(VarDecl node) { return "(" + init.getCode() + ")"; } + private String parenListInitCode(VarDecl node) { + Preconditions.checkArgument(node.getNumChildren() == 1, "Expected one child"); + ClavaNode init = node.getChild(0); + + Preconditions.checkArgument(init instanceof Expr, + "Expected an Expr, got '" + init.getClass().getSimpleName() + "'"); + + return init.getCode(); + } + private String listInitCode(VarDecl node) { // Must be present Expr initList = node.getInit().get(); @@ -111,4 +123,4 @@ private String listInitCode(VarDecl node) { // .orElseThrow(() -> new RuntimeException()); } -} \ No newline at end of file +} diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/decl/enums/Linkage.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/decl/enums/Linkage.java index 6ae4a3394..f8c27a931 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/decl/enums/Linkage.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/decl/enums/Linkage.java @@ -22,16 +22,17 @@ * */ public enum Linkage { + Invalid, /** * The entity is unique and can only be referred to from within its scope. */ - NoLinkage, + None, /** * the entity can be referred to from within the translation unit, but not other translation units. */ - InternalLinkage, + Internal, /** * External linkage within a unique namespace. @@ -41,30 +42,23 @@ public enum Linkage { * namespace, their names are unique to this translation unit, which is equivalent to having internal linkage from * the code-generation point of view. */ - UniqueExternalLinkage, + UniqueExternal, /** * No linkage according to the standard, but is visible from other translation units because of types defined in a * inline function. */ - VisibleNoLinkage, - - /** - * Internal linkage according to the Modules TS, but can be referred to from other translation units indirectly - * through inline functions and templates in the module interface. - * - */ - ModuleInternalLinkage, + VisibleNone, /** * Module linkage, which indicates that the entity can be referred to from other translation units within the same * module, and indirectly from arbitrary other translation units through inline functions and templates in the * module interface. */ - ModuleLinkage, + Module, /** * The entity can be referred to from other translation units. */ - ExternalLinkage; + External; } diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/CXXParenListInitExpr.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/CXXParenListInitExpr.java new file mode 100644 index 000000000..92f93c195 --- /dev/null +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/CXXParenListInitExpr.java @@ -0,0 +1,30 @@ +/** + * Copyright 2026 SPeCS. + *

    + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + *

    + * http://www.apache.org/licenses/LICENSE-2.0 + *

    + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package pt.up.fe.specs.clava.ast.expr; + +import java.util.Collection; + +import org.suikasoft.jOptions.Interfaces.DataStore; + +import pt.up.fe.specs.clava.ClavaNode; + +/** + * A C++20 parenthesized list-initialization expression. + */ +public class CXXParenListInitExpr extends ParenListExpr { + + public CXXParenListInitExpr(DataStore data, Collection children) { + super(data, children); + } +} diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/StringLiteral.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/StringLiteral.java index fc9b3f556..8937042ae 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/StringLiteral.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/StringLiteral.java @@ -55,6 +55,12 @@ public StringLiteral(DataStore data, Collection children) { @Override public String getLiteral() { + // Unevaluated strings can contain source-level details, such as adjacent tokens separated by a line break, that + // are lost when using the evaluated byte payload. Keep Clang's validated source spelling. + if (get(STRING_KIND) == StringKind.UNEVALUATED) { + return super.getLiteral(); + } + // Update: Unfortunately it is not possible to blindly use the source code literal // For instance, if directives and macros appear in the middle of the literal, they will also appear in the // generated source code @@ -90,7 +96,7 @@ private String getStringFromBytes() { return SpecsStrings.escapeJson(new String(bytes, kind.getCharset())); } - // If ASCII, convert each byte directly + // Ordinary strings use one-byte characters; convert each byte directly. if (kind == StringKind.ORDINARY) { var literal = new StringBuilder(); diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/enums/StringKind.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/enums/StringKind.java index afdcfbb29..8f141a1a5 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/enums/StringKind.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/expr/enums/StringKind.java @@ -18,12 +18,12 @@ import pt.up.fe.specs.util.exceptions.NotImplementedException; public enum StringKind { - ORDINARY, WIDE, UTF8(true), UTF16(true), - UTF32(true); + UTF32(true), + UNEVALUATED; private final boolean isUTF; @@ -47,6 +47,8 @@ public String getPrefix() { return "u"; case UTF32: return "U"; + case UNEVALUATED: + return ""; default: throw new NotImplementedException(this); } diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/extra/data/Language.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/extra/data/Language.java index b0a37b399..d930ebb62 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/extra/data/Language.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/extra/data/Language.java @@ -70,6 +70,11 @@ public class Language extends ADataClass { */ public static final DataKey C_PLUS_PLUS_23 = KeyFactory.bool("c++23"); + /** + * True if is a C++26 variant (or later). + */ + public static final DataKey C_PLUS_PLUS_26 = KeyFactory.bool("c++26"); + /** * True if supports digraphs. */ diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/ElaboratedType.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/ElaboratedType.java index 0772dcace..cb0809429 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/ElaboratedType.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/ElaboratedType.java @@ -63,7 +63,6 @@ public void setNamedType(Type namedType) { @Override public String getCode(ClavaNode sourceNode, String name) { - String code = getKeyword().getCode(); if (!code.isEmpty()) { code += " "; diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/UnaryTransformType.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/UnaryTransformType.java index 24c177369..827d4bf60 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/UnaryTransformType.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/UnaryTransformType.java @@ -14,6 +14,7 @@ package pt.up.fe.specs.clava.ast.type; import java.util.Collection; +import java.util.Optional; import org.suikasoft.jOptions.Datakey.DataKey; import org.suikasoft.jOptions.Datakey.KeyFactory; @@ -35,7 +36,11 @@ public class UnaryTransformType extends Type { public final static DataKey KIND = KeyFactory .enumeration("kind", UnaryTransformTypeKind.class); - public final static DataKey UNDERLYING_TYPE = KeyFactory.object("underlyingType", Type.class); + /** + * The transformed type, when Clang has resolved it. Dependent unary + * transforms can have no underlying type while still having a base type. + */ + public final static DataKey> UNDERLYING_TYPE = KeyFactory.optional("underlyingType"); public final static DataKey BASE_TYPE = KeyFactory.object("baseType", Type.class); @@ -49,7 +54,7 @@ public Type getBaseType() { return get(BASE_TYPE); } - public Type getUnderlyingType() { + public Optional getUnderlyingType() { return get(UNDERLYING_TYPE); } diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/CallingConvention.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/CallingConvention.java index b5f58e3af..47d8f46fb 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/CallingConvention.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/CallingConvention.java @@ -18,7 +18,6 @@ import pt.up.fe.specs.util.providers.StringProvider; public enum CallingConvention implements StringProvider { - C, X86StdCall, X86FastCall, @@ -39,7 +38,8 @@ public enum CallingConvention implements StringProvider { PreserveAll, AArch64VectorCall, AArch64SVEPCS, - AMDGPUKernelCall; + AMDGPUKernelCall, + M68kRTD; private static final Lazy> HELPER = EnumHelperWithValue .newLazyHelperWithValue(CallingConvention.class); diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/ElaboratedTypeKeyword.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/ElaboratedTypeKeyword.java index b9a3be4ba..3827d3650 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/ElaboratedTypeKeyword.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/ElaboratedTypeKeyword.java @@ -19,13 +19,13 @@ import pt.up.fe.specs.util.providers.StringProvider; public enum ElaboratedTypeKeyword implements StringProvider { - STRUCT, - INTERFACE, - UNION, - CLASS, - ENUM, - TYPENAME, - NONE; + Struct, + Interface, + Union, + Class, + Enum, + Typename, + None; private static final Lazy> HELPER = EnumHelperWithValue .newLazyHelperWithValue(ElaboratedTypeKeyword.class); @@ -35,7 +35,7 @@ public static EnumHelperWithValue getHelper() { } public String getCode() { - if (this == NONE) { + if (this == None) { return ""; } @@ -46,4 +46,4 @@ public String getCode() { public String getString() { return SpecsStrings.toCamelCase(name()); } -} \ No newline at end of file +} diff --git a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/UnaryTransformTypeKind.java b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/UnaryTransformTypeKind.java index 920be2ab4..7710528a1 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/UnaryTransformTypeKind.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/ast/type/enums/UnaryTransformTypeKind.java @@ -14,8 +14,21 @@ package pt.up.fe.specs.clava.ast.type.enums; public enum UnaryTransformTypeKind { - + AddLvalueReference, + AddPointer, + AddRvalueReference, Decay, + MakeSigned, + MakeUnsigned, + RemoveAllExtents, + RemoveConst, + RemoveCV, + RemoveCVRef, + RemoveExtent, + RemovePointer, + RemoveReference, + RemoveRestrict, + RemoveVolatile, EnumUnderlyingType; } diff --git a/ClavaAst/src/pt/up/fe/specs/clava/language/Standard.java b/ClavaAst/src/pt/up/fe/specs/clava/language/Standard.java index f05370f1b..b35aca2dd 100644 --- a/ClavaAst/src/pt/up/fe/specs/clava/language/Standard.java +++ b/ClavaAst/src/pt/up/fe/specs/clava/language/Standard.java @@ -33,21 +33,29 @@ public enum Standard implements StringProvider { C99, C11, C17, + C18, + C23, GNU90, GNU99, GNU11, GNU17, + GNU18, + GNU23, CXX98("c++98", true), CXX03("c++03", true), CXX11("c++11", true), CXX14("c++14", true), CXX17("c++17", true), - CXX2A("c++2a", true), + CXX20("c++20", true), + CXX23("c++23", true), + CXX26("c++26", true), GNUXX98("gnu++98", true), GNUXX11("gnu++11", true), GNUXX14("gnu++14", true), GNUXX17("gnu++17", true), - GNUXX2A("gnu++2a", true), + GNUXX20("gnu++20", true), + GNUXX23("gnu++23", true), + GNUXX26("gnu++26", true), OPENCL10("cl1.0"), OPENCL12("cl1.2"), OPENCL20("cl2.0"), @@ -63,8 +71,8 @@ public enum Standard implements StringProvider { private static final Lazy> ENUM_HELPER = EnumHelperWithValue .newLazyHelperWithValue(Standard.class); - private static final Set GNU_STANDARDS = SpecsCollections.asSet(GNU90, GNU99, GNU11, GNUXX98, GNUXX11, - GNUXX14); + private static final Set GNU_STANDARDS = SpecsCollections.asSet(GNU90, GNU99, GNU11, GNU17, GNU18, GNU23, + GNUXX98, GNUXX11, GNUXX14, GNUXX17, GNUXX20, GNUXX23, GNUXX26); public static EnumHelperWithValue getEnumHelper() { return ENUM_HELPER.get(); diff --git a/ClavaWeaver/resources/clava/test/weaver/cpp/results/TypeTemplate.js.txt b/ClavaWeaver/resources/clava/test/weaver/cpp/results/TypeTemplate.js.txt index 3030db32d..18167a321 100644 --- a/ClavaWeaver/resources/clava/test/weaver/cpp/results/TypeTemplate.js.txt +++ b/ClavaWeaver/resources/clava/test/weaver/cpp/results/TypeTemplate.js.txt @@ -10,4 +10,4 @@ Original template args: int,float After second arg to double: int,double After setting with array [double, int]: double,int After setting typedef_to_change: std::vector::const_iterator -After setting changed_typedef_type: std::vector::const_iterator \ No newline at end of file +After setting changed_typedef_type: std::vector::const_iterator diff --git a/ClavaWeaver/src/pt/up/fe/specs/clava/weaver/importable/AstFactory.java b/ClavaWeaver/src/pt/up/fe/specs/clava/weaver/importable/AstFactory.java index a0eac8c1e..df96d1bb6 100644 --- a/ClavaWeaver/src/pt/up/fe/specs/clava/weaver/importable/AstFactory.java +++ b/ClavaWeaver/src/pt/up/fe/specs/clava/weaver/importable/AstFactory.java @@ -680,7 +680,7 @@ public static ATypedefDecl typedefDecl(CxxWeaver weaver, AType underlyingT public static AElaboratedType structType(CxxWeaver weaver, AStruct struct) { var namedType = (Type) struct.getTypeImpl().getNodeImpl(); - var elaboratedType = weaver.getFactory().elaboratedType(ElaboratedTypeKeyword.STRUCT, namedType); + var elaboratedType = weaver.getFactory().elaboratedType(ElaboratedTypeKeyword.Struct, namedType); return CxxJoinpoints.create(elaboratedType, weaver, AElaboratedType.class); }