From 9ca884abfb43ee9cf54a8e40acc2f9448b30368e Mon Sep 17 00:00:00 2001 From: Dev VM Date: Tue, 23 Dec 2025 08:34:45 +0000 Subject: [PATCH 01/19] Emit naked functions as LLVM IR with inline asm instead of module asm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes issue #4294 where LTO linking fails with "symbol already defined" errors for naked template functions. The root cause was that module-level assembly from multiple compilation units gets concatenated during LTO before COMDAT deduplication can occur. The fix emits naked functions as proper LLVM IR functions with: - The 'naked' attribute (suppresses prologue/epilogue generation) - LinkOnceODRLinkage for template instantiations - COMDAT groups for proper symbol deduplication during LTO - Inline asm containing the function body - OptimizeNone and NoInline attributes to prevent LLVM from cloning the function during optimization passes (which would duplicate labels) Labels in the inline asm use printLabelName() for consistency with label references generated by the asm parser, ensuring labels are properly quoted to match the format used in jump instructions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- gen/naked.cpp | 183 +++++++++++++------------ tests/codegen/naked_asm_corner_cases.d | 130 ++++++++++++++++++ tests/codegen/naked_asm_output.d | 108 +++++++++++++++ tests/linking/asm_labels_lto.d | 91 ++++++++++++ 4 files changed, 427 insertions(+), 85 deletions(-) create mode 100644 tests/codegen/naked_asm_corner_cases.d create mode 100644 tests/codegen/naked_asm_output.d create mode 100644 tests/linking/asm_labels_lto.d diff --git a/gen/naked.cpp b/gen/naked.cpp index 5e5b3ea653d..c6fc5a799f8 100644 --- a/gen/naked.cpp +++ b/gen/naked.cpp @@ -16,6 +16,7 @@ #include "dmd/template.h" #include "gen/dvalue.h" #include "gen/funcgenstate.h" +#include "gen/functions.h" #include "gen/irstate.h" #include "gen/llvm.h" #include "gen/llvmhelpers.h" @@ -24,6 +25,7 @@ #include "ir/irfunction.h" #include "llvm/IR/InlineAsm.h" #include +#include using namespace dmd; @@ -128,9 +130,11 @@ class ToNakedIRVisitor : public Visitor { stmt->loc.toChars()); LOG_SCOPE; + // Use printLabelName to match how label references are generated in asm-x86.h. + // This ensures label definitions match the quoted format used in jump instructions. printLabelName(irs->nakedAsm, mangleExact(irs->func()->decl), stmt->ident->toChars()); - irs->nakedAsm << ":"; + irs->nakedAsm << ":\n"; if (stmt->statement) { stmt->statement->accept(this); @@ -144,108 +148,117 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { IF_LOG Logger::println("DtoDefineNakedFunction(%s)", mangleExact(fd)); LOG_SCOPE; - // we need to do special processing on the body, since we only want - // to allow actual inline asm blocks to reach the final asm output - - std::ostringstream &asmstr = gIR->nakedAsm; - - // build function header + const char *mangle = mangleExact(fd); + const auto &triple = *global.params.targetTriple; - // FIXME: could we perhaps use llvm asmwriter to give us these details ? + // Get or create the LLVM function first, before visiting the body. + // The visitor may call Declaration_codegen which needs an IR insert point. + llvm::Module &module = gIR->module; + llvm::Function *func = module.getFunction(mangle); - const char *mangle = mangleExact(fd); - std::string fullmangle; // buffer only + if (!func) { + // Create function type using the existing infrastructure + llvm::FunctionType *funcType = DtoFunctionType(fd); - const auto &triple = *global.params.targetTriple; - bool const isWin = triple.isOSWindows(); - bool const isDarwin = triple.isOSDarwin(); - - // osx is different - // also mangling has an extra underscore prefixed - if (isDarwin) { - fullmangle += '_'; - fullmangle += mangle; - mangle = fullmangle.c_str(); - - asmstr << "\t.section\t__TEXT,__text,regular,pure_instructions" - << std::endl; - asmstr << "\t.globl\t" << mangle << std::endl; + // Create function with appropriate linkage + llvm::GlobalValue::LinkageTypes linkage; if (fd->isInstantiated()) { - asmstr << "\t.weak_definition\t" << mangle << std::endl; + linkage = llvm::GlobalValue::LinkOnceODRLinkage; + } else { + linkage = llvm::GlobalValue::ExternalLinkage; } - asmstr << "\t.p2align\t4, 0x90" << std::endl; - asmstr << mangle << ":" << std::endl; + + func = llvm::Function::Create(funcType, linkage, mangle, &module); + } else if (!func->empty()) { + // Function already has a body - this can happen if the function was + // already defined (e.g., template instantiation in another module). + // Don't add another body. + return; + } else if (func->hasFnAttribute(llvm::Attribute::Naked)) { + // Function already has naked attribute - it was already processed + return; } - // Windows is different - else if (isWin) { - // mangled names starting with '?' (MSVC++ symbols) apparently need quoting - if (mangle[0] == '?') { - fullmangle += '"'; - fullmangle += mangle; - fullmangle += '"'; - mangle = fullmangle.c_str(); - } else if (triple.isArch32Bit()) { - // prepend extra underscore for Windows x86 - fullmangle += '_'; - fullmangle += mangle; - mangle = fullmangle.c_str(); - } - asmstr << "\t.def\t" << mangle << ";" << std::endl; - // hard code these two numbers for now since gas ignores .scl and llvm - // is defaulting to .type 32 for everything I have seen - asmstr << "\t.scl\t2;" << std::endl; - asmstr << "\t.type\t32;" << std::endl; - asmstr << "\t.endef" << std::endl; + // Set naked attribute - this tells LLVM not to generate prologue/epilogue + func->addFnAttr(llvm::Attribute::Naked); - if (fd->isInstantiated()) { - asmstr << "\t.section\t.text,\"xr\",discard," << mangle << std::endl; - } else { - asmstr << "\t.text" << std::endl; - } - asmstr << "\t.globl\t" << mangle << std::endl; - asmstr << "\t.p2align\t4, 0x90" << std::endl; - asmstr << mangle << ":" << std::endl; - } else { - if (fd->isInstantiated()) { - asmstr << "\t.section\t.text." << mangle << ",\"axG\",@progbits," - << mangle << ",comdat" << std::endl; - asmstr << "\t.weak\t" << mangle << std::endl; - } else { - asmstr << "\t.text" << std::endl; - asmstr << "\t.globl\t" << mangle << std::endl; - } - asmstr << "\t.p2align\t4, 0x90" << std::endl; - asmstr << "\t.type\t" << mangle << ",@function" << std::endl; - asmstr << mangle << ":" << std::endl; + // Prevent optimizations that might clone or modify the function. + // The inline asm contains labels that would conflict if duplicated. + func->addFnAttr(llvm::Attribute::OptimizeNone); + func->addFnAttr(llvm::Attribute::NoInline); + + // For template instantiations, set up COMDAT for deduplication + if (fd->isInstantiated()) { + func->setComdat(module.getOrInsertComdat(mangle)); } - // emit body - ToNakedIRVisitor v(gIR); - fd->fbody->accept(&v); + // Set other common attributes + func->addFnAttr(llvm::Attribute::NoUnwind); + + // Create entry basic block and set insert point before visiting body. + // The visitor's ExpStatement::visit may call Declaration_codegen for + // static symbols, which may need an active IR insert point. + llvm::BasicBlock *entryBB = + llvm::BasicBlock::Create(gIR->context(), "entry", func); + + // Save current insert point and switch to new function + llvm::IRBuilderBase::InsertPoint savedIP = gIR->ir->saveIP(); + gIR->ir->SetInsertPoint(entryBB); + + // Clear the nakedAsm stream and collect the function body + std::ostringstream &asmstr = gIR->nakedAsm; + asmstr.str(""); + + // Use the visitor to collect asm statements into nakedAsm + ToNakedIRVisitor visitor(gIR); + fd->fbody->accept(&visitor); - // We could have generated new errors in toNakedIR(), but we are in codegen - // already so we have to abort here. if (global.errors) { fatal(); } - // emit size after body - // llvm does this on linux, but not on osx or Win - if (!(isWin || isDarwin)) { - asmstr << "\t.size\t" << mangle << ", .-" << mangle << std::endl - << std::endl; + // Get the collected asm string and escape $ characters for LLVM inline asm. + // In LLVM inline asm, $N refers to operand N, so literal $ must be escaped as $$. + std::string asmBody; + { + std::string raw = asmstr.str(); + asmBody.reserve(raw.size() * 2); // Worst case: all $ characters + for (char c : raw) { + if (c == '$') { + asmBody += "$$"; + } else { + asmBody += c; + } + } } + asmstr.str(""); // Clear for potential reuse - gIR->module.appendModuleInlineAsm(asmstr.str()); - asmstr.str(""); + // Create inline asm - the entire function body is a single asm block + // No constraints needed since naked functions handle everything in asm + llvm::FunctionType *asmFuncType = + llvm::FunctionType::get(llvm::Type::getVoidTy(gIR->context()), false); + + llvm::InlineAsm *inlineAsm = llvm::InlineAsm::get( + asmFuncType, + asmBody, + "", // No constraints + true, // Has side effects + false, // Not align stack + llvm::InlineAsm::AD_ATT // AT&T syntax + ); + gIR->ir->CreateCall(inlineAsm); + + // Naked functions don't return normally through LLVM IR + gIR->ir->CreateUnreachable(); + + // Restore insert point + gIR->ir->restoreIP(savedIP); + + // Handle DLL export on Windows if (global.params.dllexport || - (global.params.targetTriple->isOSWindows() && fd->isExport())) { - // Embed a linker switch telling the MS linker to export the naked function. - // This mimics the effect of the dllexport attribute for regular functions. - const auto linkerSwitch = std::string("/EXPORT:") + mangle; - gIR->addLinkerOption(llvm::StringRef(linkerSwitch)); + (triple.isOSWindows() && fd->isExport())) { + func->setDLLStorageClass(llvm::GlobalValue::DLLExportStorageClass); } } @@ -436,7 +449,7 @@ DValue *DtoInlineAsmExpr(Loc loc, FuncDeclaration *fd, LLSmallVector operands; LLSmallVector indirectTypes; operands.reserve(n); - + Type *returnType = fd->type->nextOf(); const size_t cisize = constraintInfo.size(); const size_t minRequired = n + (returnType->ty == TY::Tvoid ? 0 : 1); diff --git a/tests/codegen/naked_asm_corner_cases.d b/tests/codegen/naked_asm_corner_cases.d new file mode 100644 index 00000000000..2049f9ef007 --- /dev/null +++ b/tests/codegen/naked_asm_corner_cases.d @@ -0,0 +1,130 @@ +// Tests corner cases for naked functions with DMD-style inline asm. +// +// This tests: +// 1. Stack manipulation (push/pop) +// 2. Forward and backward jumps +// 3. Nested labels +// 4. Naked function calling convention + +// REQUIRES: target_X86 + +// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-s -of=%t.s %s +// RUN: FileCheck %s --check-prefix=ASM < %t.s + +// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -run %s + +module naked_asm_corner_cases; + +// Test 1: Stack manipulation with push/pop +// ASM-LABEL: stackManipulation: +// ASM-NOT: pushq %rbp +// ASM-NOT: movq %rsp, %rbp +// ASM: pushq %rbx +// ASM: movl $42, %eax +// ASM: movl %eax, %ebx +// ASM: movl %ebx, %eax +// ASM: popq %rbx +// ASM: retq +extern(C) int stackManipulation() { + asm { naked; } + asm { + push RBX; // Save callee-saved register + mov EAX, 42; + mov EBX, EAX; // Use the saved register + mov EAX, EBX; + pop RBX; // Restore + ret; + } +} + +// Test 2: Forward jump (jump to label defined later) +// ASM-LABEL: forwardJump: +// ASM: jmp .LforwardJump_skip +// ASM: .LforwardJump_skip: +// ASM: retq +extern(C) int forwardJump() { + asm { naked; } + asm { + mov EAX, 1; + jmp skip; // Forward jump + mov EAX, 0; // Should be skipped + skip: + ret; + } +} + +// Test 3: Backward jump (loop) +// ASM-LABEL: backwardJump: +// ASM: .LbackwardJump_again: +// ASM: incl %eax +// ASM: cmpl $5, %eax +// ASM: jl .LbackwardJump_again +extern(C) int backwardJump() { + asm { naked; } + asm { + xor EAX, EAX; + again: + inc EAX; + cmp EAX, 5; + jl again; // Backward jump + ret; + } +} + +// Test 4: Multiple control flow paths +// ASM-LABEL: multiPath: +// ASM: .LmultiPath_path1: +// ASM: .LmultiPath_path2: +// ASM: .LmultiPath_done: +extern(C) int multiPath(int x) { + asm { naked; } + version(D_InlineAsm_X86_64) asm { + // x is in EDI on SysV ABI + test EDI, EDI; + jz path1; + jmp path2; + path1: + mov EAX, 10; + jmp done; + path2: + mov EAX, 20; + done: + ret; + } +} + +// Test 5: Naked function with static variable declaration (triggers Declaration_codegen) +// This tests that static declarations inside naked functions work correctly. +// The visitor's ExpStatement::visit calls Declaration_codegen for these, +// which requires an active IR insert point. +// ASM-LABEL: nakedWithStaticDecl: +// ASM: movl $42, %eax +// ASM: retq +extern(C) int nakedWithStaticDecl() { + // Static variable declaration - triggers Declaration_codegen in visitor + static immutable int staticVal = 42; + asm { naked; } + asm { + mov EAX, 42; // Use literal value since asm can't reference D variables + ret; + } +} + +// Test 6: Runtime verification +void main() { + // Verify stack manipulation works + assert(stackManipulation() == 42, "stackManipulation failed"); + + // Verify forward jump works + assert(forwardJump() == 1, "forwardJump failed"); + + // Verify backward jump (loop) works + assert(backwardJump() == 5, "backwardJump failed"); + + // Verify multi-path control flow + assert(multiPath(0) == 10, "multiPath(0) failed"); + assert(multiPath(1) == 20, "multiPath(1) failed"); + + // Verify naked function with static declaration works + assert(nakedWithStaticDecl() == 42, "nakedWithStaticDecl failed"); +} diff --git a/tests/codegen/naked_asm_output.d b/tests/codegen/naked_asm_output.d new file mode 100644 index 00000000000..465d57e8253 --- /dev/null +++ b/tests/codegen/naked_asm_output.d @@ -0,0 +1,108 @@ +// Tests that naked functions with DMD-style inline asm generate correct +// machine code without prologue/epilogue overhead. +// +// This verifies: +// 1. No function prologue (no push rbp, mov rbp rsp, etc.) +// 2. No function epilogue (no pop rbp before our explicit ret) +// 3. Labels are generated correctly +// 4. Template instantiations use proper linkage (comdat) + +// REQUIRES: target_X86 + +// Test 1: Basic naked function - verify no prologue/epilogue +// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-s -of=%t.s %s +// RUN: FileCheck %s --check-prefix=ASM < %t.s + +// Test 2: Verify LLVM IR has correct attributes +// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-ll -of=%t.ll %s +// RUN: FileCheck %s --check-prefix=IR < %t.ll + +module naked_asm_output; + +// ASM-LABEL: simpleNaked: +// ASM-NOT: pushq %rbp +// ASM-NOT: movq %rsp, %rbp +// ASM: xorl %eax, %eax +// ASM: retq +// ASM-NOT: popq %rbp + +// IR-LABEL: define i32 @simpleNaked() +// IR-SAME: #[[ATTRS:[0-9]+]] +extern(C) int simpleNaked() { + asm { naked; } + asm { + xor EAX, EAX; + ret; + } +} + +// ASM-LABEL: nakedWithLabels: +// ASM-NOT: pushq %rbp +// ASM: xorl %eax, %eax +// ASM: .LnakedWithLabels_loop: +// ASM: incl %eax +// ASM: cmpl $10, %eax +// ASM: jl .LnakedWithLabels_loop +// ASM: retq + +extern(C) int nakedWithLabels() { + asm { naked; } + asm { + xor EAX, EAX; + loop: + inc EAX; + cmp EAX, 10; + jl loop; + ret; + } +} + +// ASM-LABEL: nakedWithMultipleLabels: +// ASM-NOT: pushq %rbp +// ASM: .LnakedWithMultipleLabels_start: +// ASM: .LnakedWithMultipleLabels_middle: +// ASM: .LnakedWithMultipleLabels_end: +// ASM: retq + +extern(C) int nakedWithMultipleLabels() { + asm { naked; } + asm { + xor EAX, EAX; + start: + inc EAX; + cmp EAX, 5; + jl start; + middle: + inc EAX; + cmp EAX, 10; + jl middle; + end: + ret; + } +} + +// Template function - should have comdat for deduplication +// ASM: .section .text._D16naked_asm_output__T13nakedTemplateVii42ZQvFZi,"axG",@progbits,_D16naked_asm_output__T13nakedTemplateVii42ZQvFZi,comdat +// ASM-LABEL: _D16naked_asm_output__T13nakedTemplateVii42ZQvFZi: +// ASM-NOT: pushq %rbp +// ASM: movl $42, %eax +// ASM: retq + +// IR-LABEL: define i32 @_D16naked_asm_output__T13nakedTemplateVii42ZQvFZi() +// IR-SAME: comdat + +int nakedTemplate(int N)() { + asm { naked; } + asm { + mov EAX, N; + ret; + } +} + +// Instantiate template +int instantiate1() { + return nakedTemplate!42(); +} + +// Verify naked attribute is present in attributes group +// IR: attributes #[[ATTRS]] = {{{.*}}naked{{.*}}noinline{{.*}}nounwind{{.*}}optnone diff --git a/tests/linking/asm_labels_lto.d b/tests/linking/asm_labels_lto.d new file mode 100644 index 00000000000..dcfa1ba4b24 --- /dev/null +++ b/tests/linking/asm_labels_lto.d @@ -0,0 +1,91 @@ +// Tests that naked template functions work correctly with LTO linking. +// Previously, naked functions were emitted as module asm which caused +// duplicate symbol errors when the same template was instantiated in +// multiple modules and linked with LTO. +// +// The fix emits naked functions as LLVM IR functions with inline asm, +// allowing LLVM to properly deduplicate template instantiations. +// +// See: https://github.com/ldc-developers/ldc/issues/4294 + +// REQUIRES: LTO +// REQUIRES: target_X86 + +// RUN: split-file %s %t + +// Compile two modules that each instantiate the same naked asm template. +// RUN: %ldc -flto=full -c -I%t %t/asm_lto_user.d -of=%t/user%obj +// RUN: %ldc -flto=full -c -I%t %t/asm_lto_main.d -of=%t/main%obj + +// Link with LTO - this fails without the fix due to duplicate asm labels. +// RUN: %ldc -flto=full %t/main%obj %t/user%obj -of=%t/test%exe + +// Verify the executable runs correctly. +// RUN: %t/test%exe + +//--- asm_lto_template.d +// Template with naked function containing inline asm labels. +// This mimics std.internal.math.biguintx86 which triggers issue #4294. +// The naked function's asm becomes "module asm" which gets concatenated +// during LTO, causing duplicate symbol errors without the fix. +module asm_lto_template; + +// Template function - when instantiated in multiple modules and linked +// with LTO, the labels must have unique IDs to avoid "symbol already defined" +uint nakedAsmTemplate(int N)() { + version (D_InlineAsm_X86) { + asm { naked; } + asm { + xor EAX, EAX; + L1: + add EAX, N; + cmp EAX, 100; + jl L1; + ret; + } + } else version (D_InlineAsm_X86_64) { + asm { naked; } + asm { + xor EAX, EAX; + L1: + add EAX, N; + cmp EAX, 100; + jl L1; + ret; + } + } else { + // Fallback for non-x86 + uint result = 0; + while (result < 100) result += N; + return result; + } +} + +//--- asm_lto_user.d +// Second module that instantiates the same naked asm template. +// This creates a separate instantiation that, when linked with LTO, +// causes "symbol already defined" errors if labels aren't unique. +module asm_lto_user; + +import asm_lto_template; + +uint useTemplate() { + // Instantiate nakedAsmTemplate!1 - same as in main module + return nakedAsmTemplate!1(); +} + +//--- asm_lto_main.d +module asm_lto_main; + +import asm_lto_template; +import asm_lto_user; + +int main() { + // Both modules instantiate nakedAsmTemplate!1 + // Without unique label IDs, LTO linking fails with "symbol already defined" + uint a = nakedAsmTemplate!1(); // From this module's instantiation + uint b = useTemplate(); // From asm_lto_user's instantiation + + // Both should return the same value (>= 100) + return (a == b && a >= 100) ? 0 : 1; +} From 76f45e70e783dbbb7cd1999c8ff1c97ea5a7abea Mon Sep 17 00:00:00 2001 From: Dev VM Date: Tue, 23 Dec 2025 12:23:13 +0000 Subject: [PATCH 02/19] Fix IRBuilderHelper assertion when DtoDefineNakedFunction called at module scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use gIR->saveInsertPoint() instead of gIR->ir->saveIP() because the latter goes through IRBuilderHelper::operator->() which asserts that there's a valid insert block. At module scope (e.g., when compiling naked functions in phobos), there may not be an existing insert point. The RAII InsertPointGuard handles both null and non-null insert points correctly, saving and restoring the builder state automatically. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- gen/naked.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/gen/naked.cpp b/gen/naked.cpp index c6fc5a799f8..9b25c510866 100644 --- a/gen/naked.cpp +++ b/gen/naked.cpp @@ -201,9 +201,13 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { llvm::BasicBlock *entryBB = llvm::BasicBlock::Create(gIR->context(), "entry", func); - // Save current insert point and switch to new function - llvm::IRBuilderBase::InsertPoint savedIP = gIR->ir->saveIP(); - gIR->ir->SetInsertPoint(entryBB); + // Save current insert point and switch to new function. + // Use gIR->setInsertPoint() instead of gIR->ir->SetInsertPoint() because + // the latter goes through IRBuilderHelper::operator->() which asserts that + // there's a valid insert block. At module scope, there may not be one yet. + // gIR->setInsertPoint() accesses the builder directly and also returns an + // RAII guard that restores the previous state when it goes out of scope. + const auto savedInsertPoint = gIR->setInsertPoint(entryBB); // Clear the nakedAsm stream and collect the function body std::ostringstream &asmstr = gIR->nakedAsm; @@ -252,8 +256,8 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { // Naked functions don't return normally through LLVM IR gIR->ir->CreateUnreachable(); - // Restore insert point - gIR->ir->restoreIP(savedIP); + // The savedInsertPoint RAII guard automatically restores the insert point + // when it goes out of scope. // Handle DLL export on Windows if (global.params.dllexport || From 4b0771727ea0294f4936243f5d854baff3741350 Mon Sep 17 00:00:00 2001 From: Dev VM Date: Tue, 23 Dec 2025 13:36:50 +0000 Subject: [PATCH 03/19] Fix naked_asm_corner_cases.d test to work on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test was using -mtriple=x86_64-linux-gnu for the runtime test, which fails on macOS because it tries to link a Linux binary. Split the test requirements: - FileCheck verification: uses explicit mtriple for reproducible output - Runtime verification: uses native platform, requires host_X86 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/codegen/naked_asm_corner_cases.d | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/codegen/naked_asm_corner_cases.d b/tests/codegen/naked_asm_corner_cases.d index 2049f9ef007..d898affa910 100644 --- a/tests/codegen/naked_asm_corner_cases.d +++ b/tests/codegen/naked_asm_corner_cases.d @@ -7,11 +7,14 @@ // 4. Naked function calling convention // REQUIRES: target_X86 +// REQUIRES: host_X86 +// FileCheck verification uses explicit triple for reproducible output // RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-s -of=%t.s %s // RUN: FileCheck %s --check-prefix=ASM < %t.s -// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -run %s +// Runtime verification uses native platform +// RUN: %ldc -O0 -run %s module naked_asm_corner_cases; From 36620f0236ac8c63c3bafca25e1b035f196a85e3 Mon Sep 17 00:00:00 2001 From: Dev VM Date: Tue, 23 Dec 2025 17:21:15 +0000 Subject: [PATCH 04/19] Fix naked_asm_output.d test for flexible function ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use IR-DAG instead of IR-LABEL for template function check since template functions may be emitted after their callers in the IR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/codegen/naked_asm_output.d | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/codegen/naked_asm_output.d b/tests/codegen/naked_asm_output.d index 465d57e8253..dad156d6ff0 100644 --- a/tests/codegen/naked_asm_output.d +++ b/tests/codegen/naked_asm_output.d @@ -88,8 +88,9 @@ extern(C) int nakedWithMultipleLabels() { // ASM: movl $42, %eax // ASM: retq -// IR-LABEL: define i32 @_D16naked_asm_output__T13nakedTemplateVii42ZQvFZi() -// IR-SAME: comdat +// Template function check - use IR-DAG to allow flexible ordering since +// the template may be emitted after its caller (instantiate1) +// IR-DAG: define weak_odr i32 @_D16naked_asm_output__T13nakedTemplateVii42ZQvFZi(){{.*}}comdat int nakedTemplate(int N)() { asm { naked; } From 9dc63f81dfdd83030cb8056e148b81e2cebef4b1 Mon Sep 17 00:00:00 2001 From: Dev VM Date: Tue, 23 Dec 2025 17:21:26 +0000 Subject: [PATCH 05/19] Fix naked function symbol mangling and linkage for Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use getIRMangledName() instead of mangleExact() to get proper Windows calling convention decoration (e.g., \01 prefix for vectorcall) - Use DtoLinkage() for proper linkage determination for all function types - Fix linkage for functions already declared by DtoDeclareFunction() - Remove redundant COMDAT setup code This fixes undefined symbol errors when using naked template functions across Windows DLL boundaries. The issue was that DtoDeclareFunction() creates functions with ExternalLinkage, and DtoDefineNakedFunction() wasn't correcting the linkage or using the proper IR mangle name. Add test that cross-compiles for Windows and verifies naked template functions work correctly with DLL linking. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- gen/naked.cpp | 48 +++++++++++++++++++--------- tests/linking/naked_lambda_linkage.d | 41 ++++++++++++++++++++++++ tests/lit.site.cfg.in | 4 +++ 3 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 tests/linking/naked_lambda_linkage.d diff --git a/gen/naked.cpp b/gen/naked.cpp index 9b25c510866..6544c0cec0f 100644 --- a/gen/naked.cpp +++ b/gen/naked.cpp @@ -21,6 +21,7 @@ #include "gen/llvm.h" #include "gen/llvmhelpers.h" #include "gen/logger.h" +#include "gen/mangling.h" #include "gen/tollvm.h" #include "ir/irfunction.h" #include "llvm/IR/InlineAsm.h" @@ -148,27 +149,32 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { IF_LOG Logger::println("DtoDefineNakedFunction(%s)", mangleExact(fd)); LOG_SCOPE; - const char *mangle = mangleExact(fd); const auto &triple = *global.params.targetTriple; + // Get the proper IR mangle name (includes Windows calling convention decoration) + TypeFunction *tf = fd->type->isTypeFunction(); + const std::string irMangle = getIRMangledName(fd, tf ? tf->linkage : LINK::d); + // Get or create the LLVM function first, before visiting the body. // The visitor may call Declaration_codegen which needs an IR insert point. llvm::Module &module = gIR->module; - llvm::Function *func = module.getFunction(mangle); + llvm::Function *func = module.getFunction(irMangle); if (!func) { // Create function type using the existing infrastructure llvm::FunctionType *funcType = DtoFunctionType(fd); - // Create function with appropriate linkage - llvm::GlobalValue::LinkageTypes linkage; - if (fd->isInstantiated()) { - linkage = llvm::GlobalValue::LinkOnceODRLinkage; - } else { - linkage = llvm::GlobalValue::ExternalLinkage; - } + // Get proper linkage using the standard infrastructure. + // This correctly handles lambdas (internal linkage), templates (weak_odr), + // and other cases. + const auto lwc = DtoLinkage(fd); - func = llvm::Function::Create(funcType, linkage, mangle, &module); + func = llvm::Function::Create(funcType, lwc.first, irMangle, &module); + + // Set up COMDAT if needed (for template deduplication on ELF/Windows) + if (lwc.second) { + func->setComdat(module.getOrInsertComdat(irMangle)); + } } else if (!func->empty()) { // Function already has a body - this can happen if the function was // already defined (e.g., template instantiation in another module). @@ -177,6 +183,23 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { } else if (func->hasFnAttribute(llvm::Attribute::Naked)) { // Function already has naked attribute - it was already processed return; + } else { + // Function was declared but not yet defined (e.g., by DtoDeclareFunction). + // Fix the linkage - DtoDeclareFunction always uses ExternalLinkage, + // but naked functions need proper linkage based on their declaration type. + const auto lwc = DtoLinkage(fd); + // Clear DLL storage class before setting linkage - LLVM requires that + // functions with local linkage (internal/private) have DefaultStorageClass. + if (lwc.first == llvm::GlobalValue::InternalLinkage || + lwc.first == llvm::GlobalValue::PrivateLinkage) { + func->setDLLStorageClass(llvm::GlobalValue::DefaultStorageClass); + } + func->setLinkage(lwc.first); + if (lwc.second) { + func->setComdat(module.getOrInsertComdat(irMangle)); + } else { + func->setComdat(nullptr); + } } // Set naked attribute - this tells LLVM not to generate prologue/epilogue @@ -187,11 +210,6 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { func->addFnAttr(llvm::Attribute::OptimizeNone); func->addFnAttr(llvm::Attribute::NoInline); - // For template instantiations, set up COMDAT for deduplication - if (fd->isInstantiated()) { - func->setComdat(module.getOrInsertComdat(mangle)); - } - // Set other common attributes func->addFnAttr(llvm::Attribute::NoUnwind); diff --git a/tests/linking/naked_lambda_linkage.d b/tests/linking/naked_lambda_linkage.d new file mode 100644 index 00000000000..030d941e9ec --- /dev/null +++ b/tests/linking/naked_lambda_linkage.d @@ -0,0 +1,41 @@ +// Tests that naked template functions get correct symbol mangling on Windows. +// +// The bug: When a naked template function is called, DtoDeclareFunction creates +// a declaration with the proper IR mangle name (including \01 prefix for +// vectorcall). Without the fix, DtoDefineNakedFunction would create a definition +// with a DIFFERENT mangle name (missing the \01 prefix), leaving the declared +// function undefined. +// +// This test verifies that calling and defining a naked template in the same +// module produces a linkable object file. +// +// See: https://github.com/ldc-developers/ldc/issues/4294 + +// REQUIRES: target_X86 +// REQUIRES: atleast_llvm1600 +// REQUIRES: ld.lld + +// Compile for Windows with LTO and link into a DLL. +// LTO is critical for this test - without LTO, the linker resolves both +// symbol variants to the same name, masking the bug. +// RUN: %ldc -mtriple=x86_64-windows-msvc -betterC -flto=full -c %s -of=%t.obj +// RUN: ld.lld -flavor link /dll /noentry /export:caller %t.obj /out:%t.dll + +module naked_lambda_linkage; + +// Non-exported naked template function. +// The call in caller() triggers DtoDeclareFunction first, then the function +// is defined by DtoDefineNakedFunction. Without the fix, these use different +// mangle names and the declared symbol remains undefined. +uint nakedTemplateFunc(int N)() pure @safe @nogc { + asm pure nothrow @nogc @trusted { + naked; + mov EAX, N; + ret; + } +} + +// This function calls the naked template, triggering the declaration before definition +extern(C) export uint caller() { + return nakedTemplateFunc!42(); +} diff --git a/tests/lit.site.cfg.in b/tests/lit.site.cfg.in index 6fbfc1d3abd..edcb29712fb 100644 --- a/tests/lit.site.cfg.in +++ b/tests/lit.site.cfg.in @@ -214,6 +214,10 @@ if (platform.system() == 'Windows') and os.path.isfile( cdb ): if (platform.system() != 'Windows') and lit.util.which('gdb', config.environment['PATH']): config.available_features.add('gdb') +# Check whether ld.lld is present (for cross-platform COFF linking tests) +if lit.util.which('ld.lld', config.environment['PATH']): + config.available_features.add('ld.lld') + if 'LD_LIBRARY_PATH' in os.environ: libs = [] for lib_path in [s for s in os.environ['LD_LIBRARY_PATH'].split(':') if s]: From 130ae8cced8c90ad9dcdd885a3ea7939bc8360a0 Mon Sep 17 00:00:00 2001 From: Dev VM Date: Tue, 23 Dec 2025 20:27:05 +0000 Subject: [PATCH 06/19] Fix naked lambda dllexport assertion on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use setLinkage() and setVisibility() from tollvm.h instead of manually handling linkage and DLL storage class. This is the idiomatic pattern in LDC that correctly handles: - Lambdas (internal linkage, no dllexport) - Templates (weak_odr linkage with COMDAT) - Exported functions (dllexport on Windows) - Regular functions (external linkage) The setVisibility() function uses hasExportedLinkage() which returns false for internal/private linkage, preventing the invalid combination of local linkage with DLL storage class. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- gen/naked.cpp | 49 ++++++----------------- tests/codegen/naked_lambda_no_dllexport.d | 30 ++++++++++++++ 2 files changed, 43 insertions(+), 36 deletions(-) create mode 100644 tests/codegen/naked_lambda_no_dllexport.d diff --git a/gen/naked.cpp b/gen/naked.cpp index 6544c0cec0f..c82c3380105 100644 --- a/gen/naked.cpp +++ b/gen/naked.cpp @@ -149,8 +149,6 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { IF_LOG Logger::println("DtoDefineNakedFunction(%s)", mangleExact(fd)); LOG_SCOPE; - const auto &triple = *global.params.targetTriple; - // Get the proper IR mangle name (includes Windows calling convention decoration) TypeFunction *tf = fd->type->isTypeFunction(); const std::string irMangle = getIRMangledName(fd, tf ? tf->linkage : LINK::d); @@ -164,17 +162,10 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { // Create function type using the existing infrastructure llvm::FunctionType *funcType = DtoFunctionType(fd); - // Get proper linkage using the standard infrastructure. - // This correctly handles lambdas (internal linkage), templates (weak_odr), - // and other cases. - const auto lwc = DtoLinkage(fd); - - func = llvm::Function::Create(funcType, lwc.first, irMangle, &module); - - // Set up COMDAT if needed (for template deduplication on ELF/Windows) - if (lwc.second) { - func->setComdat(module.getOrInsertComdat(irMangle)); - } + // Create the function with ExternalLinkage initially. + // setLinkage() below will set the correct linkage. + func = llvm::Function::Create(funcType, llvm::GlobalValue::ExternalLinkage, + irMangle, &module); } else if (!func->empty()) { // Function already has a body - this can happen if the function was // already defined (e.g., template instantiation in another module). @@ -183,25 +174,17 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { } else if (func->hasFnAttribute(llvm::Attribute::Naked)) { // Function already has naked attribute - it was already processed return; - } else { - // Function was declared but not yet defined (e.g., by DtoDeclareFunction). - // Fix the linkage - DtoDeclareFunction always uses ExternalLinkage, - // but naked functions need proper linkage based on their declaration type. - const auto lwc = DtoLinkage(fd); - // Clear DLL storage class before setting linkage - LLVM requires that - // functions with local linkage (internal/private) have DefaultStorageClass. - if (lwc.first == llvm::GlobalValue::InternalLinkage || - lwc.first == llvm::GlobalValue::PrivateLinkage) { - func->setDLLStorageClass(llvm::GlobalValue::DefaultStorageClass); - } - func->setLinkage(lwc.first); - if (lwc.second) { - func->setComdat(module.getOrInsertComdat(irMangle)); - } else { - func->setComdat(nullptr); - } } + // Set linkage and visibility using the standard infrastructure. + // This correctly handles: + // - Lambdas (internal linkage, no dllexport) + // - Templates (weak_odr linkage with COMDAT) + // - Exported functions (dllexport on Windows) + // - Regular functions (external linkage) + setLinkage(DtoLinkage(fd), func); + setVisibility(fd, func); + // Set naked attribute - this tells LLVM not to generate prologue/epilogue func->addFnAttr(llvm::Attribute::Naked); @@ -276,12 +259,6 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) { // The savedInsertPoint RAII guard automatically restores the insert point // when it goes out of scope. - - // Handle DLL export on Windows - if (global.params.dllexport || - (triple.isOSWindows() && fd->isExport())) { - func->setDLLStorageClass(llvm::GlobalValue::DLLExportStorageClass); - } } //////////////////////////////////////////////////////////////////////////////// diff --git a/tests/codegen/naked_lambda_no_dllexport.d b/tests/codegen/naked_lambda_no_dllexport.d new file mode 100644 index 00000000000..5b086cfeb1a --- /dev/null +++ b/tests/codegen/naked_lambda_no_dllexport.d @@ -0,0 +1,30 @@ +// Tests that naked lambda functions don't get dllexport with internal linkage. +// +// Lambda functions get internal linkage, and internal linkage is incompatible +// with dllexport storage class. This caused an assertion failure: +// "local linkage requires DefaultStorageClass" +// +// REQUIRES: target_X86 + +// RUN: %ldc -mtriple=x86_64-windows-msvc -fvisibility=public -c %s --output-ll -of=%t.ll + +// The lambda should have internal linkage but NOT dllexport. +// Bug: "define internal dllexport" - invalid IR that fails LLVM verification. +// Use LLVM opt verifier to check for invalid IR. +// RUN: opt -passes=verify -S %t.ll -o /dev/null + +module naked_lambda_no_dllexport; + +void caller() { + // Function literal (lambda) with naked asm gets internal linkage. + // With -fvisibility=public, this must NOT get dllexport. + auto nakedLambda = () { + asm { + naked; + xor EAX, EAX; + ret; + } + }; + + nakedLambda(); +} From f475f9e2bc92f8c5c7d91b174605f981ed286abf Mon Sep 17 00:00:00 2001 From: Dev VM Date: Tue, 23 Dec 2025 22:11:51 +0000 Subject: [PATCH 07/19] Fix naked_asm_corner_cases.d test for Windows and 32-bit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use version(D_InlineAsm_X86_64) and version(D_InlineAsm_X86) blocks to handle both 64-bit and 32-bit x86 platforms - Use ECX for first argument on Windows x64 ABI (not EDI like SysV) - Use [ESP+4] for first argument on 32-bit cdecl calling convention - Provide fallback return values for non-x86 platforms 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/codegen/naked_asm_corner_cases.d | 166 +++++++++++++++++++------ 1 file changed, 125 insertions(+), 41 deletions(-) diff --git a/tests/codegen/naked_asm_corner_cases.d b/tests/codegen/naked_asm_corner_cases.d index d898affa910..5805d91d1ba 100644 --- a/tests/codegen/naked_asm_corner_cases.d +++ b/tests/codegen/naked_asm_corner_cases.d @@ -13,7 +13,7 @@ // RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-s -of=%t.s %s // RUN: FileCheck %s --check-prefix=ASM < %t.s -// Runtime verification uses native platform +// Runtime verification uses native platform (only on 64-bit) // RUN: %ldc -O0 -run %s module naked_asm_corner_cases; @@ -29,15 +29,29 @@ module naked_asm_corner_cases; // ASM: popq %rbx // ASM: retq extern(C) int stackManipulation() { - asm { naked; } - asm { - push RBX; // Save callee-saved register - mov EAX, 42; - mov EBX, EAX; // Use the saved register - mov EAX, EBX; - pop RBX; // Restore - ret; + version(D_InlineAsm_X86_64) { + asm { naked; } + asm { + push RBX; // Save callee-saved register + mov EAX, 42; + mov EBX, EAX; // Use the saved register + mov EAX, EBX; + pop RBX; // Restore + ret; + } } + else version(D_InlineAsm_X86) { + asm { naked; } + asm { + push EBX; // Save callee-saved register + mov EAX, 42; + mov EBX, EAX; // Use the saved register + mov EAX, EBX; + pop EBX; // Restore + ret; + } + } + else return 42; // Fallback for non-x86 } // Test 2: Forward jump (jump to label defined later) @@ -46,14 +60,27 @@ extern(C) int stackManipulation() { // ASM: .LforwardJump_skip: // ASM: retq extern(C) int forwardJump() { - asm { naked; } - asm { - mov EAX, 1; - jmp skip; // Forward jump - mov EAX, 0; // Should be skipped - skip: - ret; + version(D_InlineAsm_X86_64) { + asm { naked; } + asm { + mov EAX, 1; + jmp skip; // Forward jump + mov EAX, 0; // Should be skipped + skip: + ret; + } + } + else version(D_InlineAsm_X86) { + asm { naked; } + asm { + mov EAX, 1; + jmp skip; + mov EAX, 0; + skip: + ret; + } } + else return 1; } // Test 3: Backward jump (loop) @@ -63,15 +90,29 @@ extern(C) int forwardJump() { // ASM: cmpl $5, %eax // ASM: jl .LbackwardJump_again extern(C) int backwardJump() { - asm { naked; } - asm { - xor EAX, EAX; - again: - inc EAX; - cmp EAX, 5; - jl again; // Backward jump - ret; + version(D_InlineAsm_X86_64) { + asm { naked; } + asm { + xor EAX, EAX; + again: + inc EAX; + cmp EAX, 5; + jl again; // Backward jump + ret; + } + } + else version(D_InlineAsm_X86) { + asm { naked; } + asm { + xor EAX, EAX; + again: + inc EAX; + cmp EAX, 5; + jl again; + ret; + } } + else return 5; } // Test 4: Multiple control flow paths @@ -80,20 +121,53 @@ extern(C) int backwardJump() { // ASM: .LmultiPath_path2: // ASM: .LmultiPath_done: extern(C) int multiPath(int x) { - asm { naked; } - version(D_InlineAsm_X86_64) asm { - // x is in EDI on SysV ABI - test EDI, EDI; - jz path1; - jmp path2; - path1: - mov EAX, 10; - jmp done; - path2: - mov EAX, 20; - done: - ret; + version(D_InlineAsm_X86_64) { + asm { naked; } + version(Windows) asm { + // x is in ECX on Windows x64 ABI + test ECX, ECX; + jz path1; + jmp path2; + path1: + mov EAX, 10; + jmp done; + path2: + mov EAX, 20; + done: + ret; + } + else asm { + // x is in EDI on SysV ABI + test EDI, EDI; + jz path1; + jmp path2; + path1: + mov EAX, 10; + jmp done; + path2: + mov EAX, 20; + done: + ret; + } } + else version(D_InlineAsm_X86) { + asm { naked; } + asm { + // x is on stack at [ESP+4] for 32-bit cdecl + mov EAX, [ESP+4]; + test EAX, EAX; + jz path1; + jmp path2; + path1: + mov EAX, 10; + jmp done; + path2: + mov EAX, 20; + done: + ret; + } + } + else return x == 0 ? 10 : 20; } // Test 5: Naked function with static variable declaration (triggers Declaration_codegen) @@ -106,11 +180,21 @@ extern(C) int multiPath(int x) { extern(C) int nakedWithStaticDecl() { // Static variable declaration - triggers Declaration_codegen in visitor static immutable int staticVal = 42; - asm { naked; } - asm { - mov EAX, 42; // Use literal value since asm can't reference D variables - ret; + version(D_InlineAsm_X86_64) { + asm { naked; } + asm { + mov EAX, 42; // Use literal value since asm can't reference D variables + ret; + } + } + else version(D_InlineAsm_X86) { + asm { naked; } + asm { + mov EAX, 42; + ret; + } } + else return 42; } // Test 6: Runtime verification From c7907b78da6493826d9de1cdaf6d7edf38b0e538 Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Mon, 12 Jan 2026 11:45:29 +0100 Subject: [PATCH 08/19] WIP: Remove DtoDefineNakedFunction() --- gen/asmstmt.cpp | 18 ---- gen/functions.cpp | 32 +++---- gen/functions.h | 1 - gen/irstate.h | 1 - gen/naked.cpp | 231 ---------------------------------------------- 5 files changed, 12 insertions(+), 271 deletions(-) diff --git a/gen/asmstmt.cpp b/gen/asmstmt.cpp index 12d364d012d..94233aee0a7 100644 --- a/gen/asmstmt.cpp +++ b/gen/asmstmt.cpp @@ -784,21 +784,3 @@ void CompoundAsmStatement_toIR(CompoundAsmStatement *stmt, IRState *p) { p->ir->SetInsertPoint(bb); } } - -////////////////////////////////////////////////////////////////////////////// - -void AsmStatement_toNakedIR(InlineAsmStatement *stmt, IRState *irs) { - IF_LOG Logger::println("InlineAsmStatement::toNakedIR(): %s", - stmt->loc.toChars()); - LOG_SCOPE; - - // is there code? - if (!stmt->asmcode) { - return; - } - AsmCode *code = static_cast(stmt->asmcode); - - // build asm stmt - replace_func_name(irs, code->insnTemplate); - irs->nakedAsm << "\t" << code->insnTemplate << std::endl; -} diff --git a/gen/functions.cpp b/gen/functions.cpp index 68791706712..2e1b98cfd1c 100644 --- a/gen/functions.cpp +++ b/gen/functions.cpp @@ -1127,12 +1127,6 @@ void DtoDefineFunction(FuncDeclaration *fd, bool linkageAvailableExternally) { gIR->funcGenStates.pop_back(); }; - // if this function is naked, we take over right away! no standard processing! - if (fd->isNaked()) { - DtoDefineNakedFunction(fd); - return; - } - SCOPE_EXIT { if (irFunc->isDynamicCompiled()) { defineDynamicCompiledFunction(gIR, irFunc); @@ -1210,18 +1204,13 @@ void DtoDefineFunction(FuncDeclaration *fd, bool linkageAvailableExternally) { gIR->ir->setFastMathFlags(irFunc->FMF); gIR->DBuilder.EmitFuncStart(fd); - // @naked: emit body and return, no prologue/epilogue - if (func->hasFnAttribute(llvm::Attribute::Naked)) { - Statement_toIR(fd->fbody, gIR); - const bool wasDummy = eraseDummyAfterReturnBB(gIR->scopebb()); - if (!wasDummy && !gIR->scopereturned()) { - // this is what clang does to prevent LLVM complaining about - // non-terminated function - gIR->ir->CreateUnreachable(); - } - return; + if (fd->isNaked()) { // DMD-style `asm { naked; }` + func->addFnAttr(llvm::Attribute::Naked); } + // @naked: emit body and return, no prologue/epilogue + const bool isNaked = func->hasFnAttribute(llvm::Attribute::Naked); + // create alloca point // this gets erased when the function is complete, so alignment etc does not // matter at all @@ -1234,12 +1223,12 @@ void DtoDefineFunction(FuncDeclaration *fd, bool linkageAvailableExternally) { emitInstrumentationFnEnter(fd); if (global.params.trace && fd->emitInstrumentation && !fd->isCMain() && - !fd->isNaked()) { + !isNaked) { emitDMDStyleFunctionTrace(*gIR, fd, funcGen); } // give the 'this' parameter (an lvalue) storage and debug info - if (irFty.arg_this) { + if (!isNaked && irFty.arg_this) { LLValue *thisvar = irFunc->thisArg; assert(thisvar); @@ -1261,7 +1250,7 @@ void DtoDefineFunction(FuncDeclaration *fd, bool linkageAvailableExternally) { } // define all explicit parameters - if (fd->parameters) + if (!isNaked && fd->parameters) defineParameters(irFty, *fd->parameters); // Initialize PGO state for this function @@ -1327,7 +1316,10 @@ void DtoDefineFunction(FuncDeclaration *fd, bool linkageAvailableExternally) { // pass the previous block into this block gIR->DBuilder.EmitStopPoint(fd->endloc); - if (func->getReturnType() == LLType::getVoidTy(gIR->context())) { + + if (isNaked) { + gIR->ir->CreateUnreachable(); + } else if (func->getReturnType() == LLType::getVoidTy(gIR->context())) { gIR->ir->CreateRetVoid(); } else if (isAnyMainFunction(fd)) { gIR->ir->CreateRet(LLConstant::getNullValue(func->getReturnType())); diff --git a/gen/functions.h b/gen/functions.h index 5dcfa7032dc..609da95620d 100644 --- a/gen/functions.h +++ b/gen/functions.h @@ -40,7 +40,6 @@ void DtoResolveFunction(FuncDeclaration *fdecl); void DtoDeclareFunction(FuncDeclaration *fdecl); void DtoDefineFunction(FuncDeclaration *fd, bool linkageAvailableExternally = false); -void DtoDefineNakedFunction(FuncDeclaration *fd); void emitABIReturnAsmStmt(IRAsmBlock *asmblock, Loc loc, FuncDeclaration *fdecl); diff --git a/gen/irstate.h b/gen/irstate.h index 8004a0c83f1..721db8dc3f9 100644 --- a/gen/irstate.h +++ b/gen/irstate.h @@ -215,7 +215,6 @@ struct IRState { // for inline asm IRAsmBlock *asmBlock = nullptr; - std::ostringstream nakedAsm; // Globals to pin in the llvm.used array to make sure they are not // eliminated. diff --git a/gen/naked.cpp b/gen/naked.cpp index c82c3380105..5a458c04704 100644 --- a/gen/naked.cpp +++ b/gen/naked.cpp @@ -30,237 +30,6 @@ using namespace dmd; -//////////////////////////////////////////////////////////////////////////////// -// FIXME: Integrate these functions -void AsmStatement_toNakedIR(InlineAsmStatement *stmt, IRState *irs); - -//////////////////////////////////////////////////////////////////////////////// - -class ToNakedIRVisitor : public Visitor { - IRState *irs; - -public: - explicit ToNakedIRVisitor(IRState *irs) : irs(irs) {} - - ////////////////////////////////////////////////////////////////////////// - - // Import all functions from class Visitor - using Visitor::visit; - - ////////////////////////////////////////////////////////////////////////// - - void visit(Statement *stmt) override { - error(stmt->loc, "Statement not allowed in naked function"); - } - - ////////////////////////////////////////////////////////////////////////// - - void visit(InlineAsmStatement *stmt) override { - AsmStatement_toNakedIR(stmt, irs); - } - - ////////////////////////////////////////////////////////////////////////// - - void visit(CompoundStatement *stmt) override { - IF_LOG Logger::println("CompoundStatement::toNakedIR(): %s", - stmt->loc.toChars()); - LOG_SCOPE; - - if (stmt->statements) { - for (auto s : *stmt->statements) { - if (s) { - s->accept(this); - } - } - } - } - - ////////////////////////////////////////////////////////////////////////// - - void visit(ExpStatement *stmt) override { - IF_LOG Logger::println("ExpStatement::toNakedIR(): %s", - stmt->loc.toChars()); - LOG_SCOPE; - - // This happens only if there is a ; at the end: - // asm { naked; ... }; - // Is this a legal AST? - if (!stmt->exp) { - return; - } - - // only expstmt supported in declarations - if (!stmt->exp || stmt->exp->op != EXP::declaration) { - visit(static_cast(stmt)); - return; - } - - DeclarationExp *d = static_cast(stmt->exp); - VarDeclaration *vd = d->declaration->isVarDeclaration(); - FuncDeclaration *fd = d->declaration->isFuncDeclaration(); - EnumDeclaration *ed = d->declaration->isEnumDeclaration(); - - // and only static variable/function declaration - // no locals or nested stuffies! - if (!vd && !fd && !ed) { - visit(static_cast(stmt)); - return; - } - if (vd && !(vd->storage_class & (STCstatic | STCmanifest))) { - error(vd->loc, "non-static variable `%s` not allowed in naked function", - vd->toChars()); - return; - } - if (fd && !fd->isStatic()) { - error(fd->loc, - "non-static nested function `%s` not allowed in naked function", - fd->toChars()); - return; - } - // enum decls should always be safe - - // make sure the symbols gets processed - // TODO: codegen() here is likely incorrect - Declaration_codegen(d->declaration, irs); - } - - ////////////////////////////////////////////////////////////////////////// - - void visit(LabelStatement *stmt) override { - IF_LOG Logger::println("LabelStatement::toNakedIR(): %s", - stmt->loc.toChars()); - LOG_SCOPE; - - // Use printLabelName to match how label references are generated in asm-x86.h. - // This ensures label definitions match the quoted format used in jump instructions. - printLabelName(irs->nakedAsm, mangleExact(irs->func()->decl), - stmt->ident->toChars()); - irs->nakedAsm << ":\n"; - - if (stmt->statement) { - stmt->statement->accept(this); - } - } -}; - -//////////////////////////////////////////////////////////////////////////////// - -void DtoDefineNakedFunction(FuncDeclaration *fd) { - IF_LOG Logger::println("DtoDefineNakedFunction(%s)", mangleExact(fd)); - LOG_SCOPE; - - // Get the proper IR mangle name (includes Windows calling convention decoration) - TypeFunction *tf = fd->type->isTypeFunction(); - const std::string irMangle = getIRMangledName(fd, tf ? tf->linkage : LINK::d); - - // Get or create the LLVM function first, before visiting the body. - // The visitor may call Declaration_codegen which needs an IR insert point. - llvm::Module &module = gIR->module; - llvm::Function *func = module.getFunction(irMangle); - - if (!func) { - // Create function type using the existing infrastructure - llvm::FunctionType *funcType = DtoFunctionType(fd); - - // Create the function with ExternalLinkage initially. - // setLinkage() below will set the correct linkage. - func = llvm::Function::Create(funcType, llvm::GlobalValue::ExternalLinkage, - irMangle, &module); - } else if (!func->empty()) { - // Function already has a body - this can happen if the function was - // already defined (e.g., template instantiation in another module). - // Don't add another body. - return; - } else if (func->hasFnAttribute(llvm::Attribute::Naked)) { - // Function already has naked attribute - it was already processed - return; - } - - // Set linkage and visibility using the standard infrastructure. - // This correctly handles: - // - Lambdas (internal linkage, no dllexport) - // - Templates (weak_odr linkage with COMDAT) - // - Exported functions (dllexport on Windows) - // - Regular functions (external linkage) - setLinkage(DtoLinkage(fd), func); - setVisibility(fd, func); - - // Set naked attribute - this tells LLVM not to generate prologue/epilogue - func->addFnAttr(llvm::Attribute::Naked); - - // Prevent optimizations that might clone or modify the function. - // The inline asm contains labels that would conflict if duplicated. - func->addFnAttr(llvm::Attribute::OptimizeNone); - func->addFnAttr(llvm::Attribute::NoInline); - - // Set other common attributes - func->addFnAttr(llvm::Attribute::NoUnwind); - - // Create entry basic block and set insert point before visiting body. - // The visitor's ExpStatement::visit may call Declaration_codegen for - // static symbols, which may need an active IR insert point. - llvm::BasicBlock *entryBB = - llvm::BasicBlock::Create(gIR->context(), "entry", func); - - // Save current insert point and switch to new function. - // Use gIR->setInsertPoint() instead of gIR->ir->SetInsertPoint() because - // the latter goes through IRBuilderHelper::operator->() which asserts that - // there's a valid insert block. At module scope, there may not be one yet. - // gIR->setInsertPoint() accesses the builder directly and also returns an - // RAII guard that restores the previous state when it goes out of scope. - const auto savedInsertPoint = gIR->setInsertPoint(entryBB); - - // Clear the nakedAsm stream and collect the function body - std::ostringstream &asmstr = gIR->nakedAsm; - asmstr.str(""); - - // Use the visitor to collect asm statements into nakedAsm - ToNakedIRVisitor visitor(gIR); - fd->fbody->accept(&visitor); - - if (global.errors) { - fatal(); - } - - // Get the collected asm string and escape $ characters for LLVM inline asm. - // In LLVM inline asm, $N refers to operand N, so literal $ must be escaped as $$. - std::string asmBody; - { - std::string raw = asmstr.str(); - asmBody.reserve(raw.size() * 2); // Worst case: all $ characters - for (char c : raw) { - if (c == '$') { - asmBody += "$$"; - } else { - asmBody += c; - } - } - } - asmstr.str(""); // Clear for potential reuse - - // Create inline asm - the entire function body is a single asm block - // No constraints needed since naked functions handle everything in asm - llvm::FunctionType *asmFuncType = - llvm::FunctionType::get(llvm::Type::getVoidTy(gIR->context()), false); - - llvm::InlineAsm *inlineAsm = llvm::InlineAsm::get( - asmFuncType, - asmBody, - "", // No constraints - true, // Has side effects - false, // Not align stack - llvm::InlineAsm::AD_ATT // AT&T syntax - ); - - gIR->ir->CreateCall(inlineAsm); - - // Naked functions don't return normally through LLVM IR - gIR->ir->CreateUnreachable(); - - // The savedInsertPoint RAII guard automatically restores the insert point - // when it goes out of scope. -} - //////////////////////////////////////////////////////////////////////////////// void emitABIReturnAsmStmt(IRAsmBlock *asmblock, Loc loc, From 7f16e37496e38791c3e4c72f89b010a475bee035 Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Fri, 6 Feb 2026 19:04:39 +0100 Subject: [PATCH 09/19] WIP: Hack away special cases for *naked* DMD-style inline asm --- gen/asm-x86.h | 28 ++++++++++++++++++++-------- gen/asmstmt.cpp | 2 ++ gen/functions.cpp | 6 ++---- tests/codegen/naked_asm_output.d | 2 +- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/gen/asm-x86.h b/gen/asm-x86.h index eecd1a8b654..dd21aa45ed4 100644 --- a/gen/asm-x86.h +++ b/gen/asm-x86.h @@ -2340,7 +2340,7 @@ struct AsmProcessor { AsmCode *asmcode, AsmArgMode mode = Mode_Input) { using namespace dmd; - if (sc->func->isNaked()) { + if (false && sc->func->isNaked()) { switch (type) { case Arg_Integer: if (e->type->isUnsigned()) { @@ -2392,16 +2392,28 @@ struct AsmProcessor { llvm_unreachable("Unsupported argument in asm."); break; } - } else { - insnTemplate << fmt << "<<" << (mode == Mode_Input ? "in" : "out") - << asmcode->args.size() << ">>"; - asmcode->args.push_back(AsmArg(type, e, mode)); + } else { // non-naked + if (type == Arg_Integer) { + if (e->type->isUnsigned()) { + insnTemplate << "$$" << e->toUInteger(); + } else { +#ifndef ASM_X86_64 + insnTemplate << "$$" << static_cast(e->toInteger()); +#else + insnTemplate << "$$" << e->toInteger(); +#endif + } + } else { + insnTemplate << fmt << "<<" << (mode == Mode_Input ? "in" : "out") + << asmcode->args.size() << ">>"; + asmcode->args.push_back(AsmArg(type, e, mode)); + } } } void addOperand2(const char *fmtpre, const char *fmtpost, AsmArgType type, Expression *e, AsmCode *asmcode, AsmArgMode mode = Mode_Input) { - if (sc->func->isNaked()) { + if (false && sc->func->isNaked()) { // taken from above error(stmt->loc, "only global variables can be referenced by identifier in " "naked asm"); @@ -3081,7 +3093,7 @@ struct AsmProcessor { // (Only for non-naked asm, as this isn't an issue for naked asm.) // // See also: https://lists.llvm.org/pipermail/llvm-dev/2017-August/116244.html - const auto forceLeadingDisplacement = hasConstDisplacement && !sc->func->isNaked(); + const auto forceLeadingDisplacement = hasConstDisplacement;// && !sc->func->isNaked(); if (forceLeadingDisplacement) { // Subtract 8 from our const-displacement, and prepare to add the 8 from the `H` modifier. insnTemplate << "-8+"; @@ -3092,7 +3104,7 @@ struct AsmProcessor { use_star = false; } - if (!sc->func->isNaked()) // no addrexp in naked asm please :) + if (true || !sc->func->isNaked()) // no addrexp in naked asm please :) { Type *tt = pointerTo(e->type); e = createAddrExp(Loc(), e); diff --git a/gen/asmstmt.cpp b/gen/asmstmt.cpp index 94233aee0a7..eca13c722eb 100644 --- a/gen/asmstmt.cpp +++ b/gen/asmstmt.cpp @@ -538,6 +538,7 @@ void CompoundAsmStatement_toIR(CompoundAsmStatement *stmt, IRState *p) { continue; } + /* // if we already set things up for this branch target, skip if (gotoToVal.find(targetLabel) != gotoToVal.end()) { continue; @@ -561,6 +562,7 @@ void CompoundAsmStatement_toIR(CompoundAsmStatement *stmt, IRState *p) { code << asmGotoEnd; ++n_goto; + */ } if (code.str() != asmGotoEnd) { // finalize code diff --git a/gen/functions.cpp b/gen/functions.cpp index 2e1b98cfd1c..8d79b4ed84a 100644 --- a/gen/functions.cpp +++ b/gen/functions.cpp @@ -773,10 +773,8 @@ static LinkageWithCOMDAT lowerFuncLinkage(FuncDeclaration *fdecl) { return LinkageWithCOMDAT(LLGlobalValue::ExternalLinkage, false); } - // A body-less declaration always needs to be marked as external in LLVM - // (also e.g. naked template functions which would otherwise be weak_odr, - // but where the definition is in module-level inline asm). - if (!fdecl->fbody || fdecl->isNaked()) { + // A body-less declaration always needs to be marked as external in LLVM. + if (!fdecl->fbody) { return LinkageWithCOMDAT(LLGlobalValue::ExternalLinkage, false); } diff --git a/tests/codegen/naked_asm_output.d b/tests/codegen/naked_asm_output.d index dad156d6ff0..4a5f6b84cc5 100644 --- a/tests/codegen/naked_asm_output.d +++ b/tests/codegen/naked_asm_output.d @@ -106,4 +106,4 @@ int instantiate1() { } // Verify naked attribute is present in attributes group -// IR: attributes #[[ATTRS]] = {{{.*}}naked{{.*}}noinline{{.*}}nounwind{{.*}}optnone +// IR: attributes #[[ATTRS]] = {{{.*}}naked{{.*}}noinline From 3b590046cf71d1173cb72bda2d300f23cb11f4aa Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 7 Feb 2026 01:14:01 +0100 Subject: [PATCH 10/19] [restore support for jumping to non-asm labels] --- dmd/declaration.h | 3 +++ dmd/func.d | 4 ++++ dmd/statementsem.d | 7 +++++++ gen/asmstmt.cpp | 9 ++++----- gen/irstate.h | 3 --- gen/statements.cpp | 1 - 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/dmd/declaration.h b/dmd/declaration.h index 75333483745..6f94b76b54a 100644 --- a/dmd/declaration.h +++ b/dmd/declaration.h @@ -581,6 +581,9 @@ class FuncDeclaration : public Declaration VarDeclaration *v_argptr; // '_argptr' variable VarDeclarations *parameters; // Array of VarDeclaration's for parameters DsymbolTable *labtab; // statement label symbol table +#if IN_LLVM + Identifiers *asmLabels; +#endif Dsymbol *overnext; // next in overload list FuncDeclaration *overnext0; // next in overload list (only used during IFTI) Loc endloc; // location of closing curly bracket diff --git a/dmd/func.d b/dmd/func.d index 6709e32ebb0..e39e233eaef 100644 --- a/dmd/func.d +++ b/dmd/func.d @@ -256,6 +256,10 @@ version (IN_LLVM) VarDeclaration v_argptr; /// '_argptr' variable VarDeclarations* parameters; /// Array of VarDeclaration's for parameters DsymbolTable labtab; /// statement label symbol table +version (IN_LLVM) +{ + Identifiers* asmLabels; /// identifiers of labels defined in DMD-style inline assembly +} Dsymbol overnext; /// next in overload list FuncDeclaration overnext0; /// next in overload list (only used during IFTI) Loc endloc; /// location of closing curly bracket diff --git a/dmd/statementsem.d b/dmd/statementsem.d index 8df6a15b245..1bb4b82a381 100644 --- a/dmd/statementsem.d +++ b/dmd/statementsem.d @@ -3667,6 +3667,13 @@ version (IN_LLVM) if (auto ls = s.isLabelStatement()) { sc.func.searchLabel(ls.ident, ls.loc); + +version (IN_LLVM) +{ + if (!sc.func.asmLabels) + sc.func.asmLabels = new Identifiers(); + sc.func.asmLabels.push(ls.ident); +} } } } diff --git a/gen/asmstmt.cpp b/gen/asmstmt.cpp index eca13c722eb..6a67efe08be 100644 --- a/gen/asmstmt.cpp +++ b/gen/asmstmt.cpp @@ -532,13 +532,13 @@ void CompoundAsmStatement_toIR(CompoundAsmStatement *stmt, IRState *p) { } Identifier *const ident = targetLabel->ident; - // if internal, no special handling is necessary, skip - if (llvm::any_of(asmblock->internalLabels, - [ident](Identifier *i) { return i->equals(ident); })) { + // if the label is defined in inline asm, no special handling is necessary, skip + if (fd->asmLabels && llvm::any_of(*fd->asmLabels, [ident](Identifier *i) { + return i->equals(ident); + })) { continue; } - /* // if we already set things up for this branch target, skip if (gotoToVal.find(targetLabel) != gotoToVal.end()) { continue; @@ -562,7 +562,6 @@ void CompoundAsmStatement_toIR(CompoundAsmStatement *stmt, IRState *p) { code << asmGotoEnd; ++n_goto; - */ } if (code.str() != asmGotoEnd) { // finalize code diff --git a/gen/irstate.h b/gen/irstate.h index 721db8dc3f9..323f87a1521 100644 --- a/gen/irstate.h +++ b/gen/irstate.h @@ -94,9 +94,6 @@ struct IRAsmBlock { std::set clobs; size_t outputcount; - // stores the labels within the asm block - std::vector internalLabels; - CompoundAsmStatement *asmBlock; LLType *retty; unsigned retn; diff --git a/gen/statements.cpp b/gen/statements.cpp index 1bdd3cef303..ab0fd31fe61 100644 --- a/gen/statements.cpp +++ b/gen/statements.cpp @@ -1552,7 +1552,6 @@ class ToIRVisitor : public Visitor { label << ":"; a->code = label.str(); irs->asmBlock->s.push_back(a); - irs->asmBlock->internalLabels.push_back(stmt->ident); // disable inlining irs->func()->setNeverInline(); From 62c09e4d81bc0aaa70da3bd91ceeb0caad467e16 Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 7 Feb 2026 01:38:08 +0100 Subject: [PATCH 11/19] [little cleanup] --- gen/asm-x86.h | 96 ++++++++------------------------------------------- gen/naked.cpp | 3 -- 2 files changed, 15 insertions(+), 84 deletions(-) diff --git a/gen/asm-x86.h b/gen/asm-x86.h index dd21aa45ed4..2104b4155c1 100644 --- a/gen/asm-x86.h +++ b/gen/asm-x86.h @@ -2340,86 +2340,25 @@ struct AsmProcessor { AsmCode *asmcode, AsmArgMode mode = Mode_Input) { using namespace dmd; - if (false && sc->func->isNaked()) { - switch (type) { - case Arg_Integer: - if (e->type->isUnsigned()) { - insnTemplate << "$" << e->toUInteger(); - } else { -#ifndef ASM_X86_64 - insnTemplate << "$" << static_cast(e->toInteger()); -#else - insnTemplate << "$" << e->toInteger(); -#endif - } - break; - - case Arg_Pointer: - error(stmt->loc, "unsupported pointer reference to `%s` in naked asm", - e->toChars()); - break; - - case Arg_Memory: - // Peel off one layer of explicitly taking the address, if present. - if (auto ae = e->isAddrExp()) { - e = ae->e1; - } - - if (auto v = e->isVarExp()) { - if (VarDeclaration *vd = v->var->isVarDeclaration()) { - if (!vd->isDataseg()) { - error(stmt->loc, "only global variables can be referenced by " - "identifier in naked asm"); - break; - } - - // print out the mangle - if (prependExtraUnderscore(vd->resolvedLinkage())) { - insnTemplate << "_"; - } - OutBuffer buf; - mangleToBuffer(vd, buf); - insnTemplate << buf.peekChars(); - getIrGlobal(vd, true)->nakedUse = true; - break; - } - } - error(stmt->loc, "unsupported memory reference to `%s` in naked asm", - e->toChars()); - break; - - default: - llvm_unreachable("Unsupported argument in asm."); - break; - } - } else { // non-naked - if (type == Arg_Integer) { - if (e->type->isUnsigned()) { - insnTemplate << "$$" << e->toUInteger(); - } else { + if (type == Arg_Integer) { + if (e->type->isUnsigned()) { + insnTemplate << "$$" << e->toUInteger(); + } else { #ifndef ASM_X86_64 - insnTemplate << "$$" << static_cast(e->toInteger()); + insnTemplate << "$$" << static_cast(e->toInteger()); #else - insnTemplate << "$$" << e->toInteger(); + insnTemplate << "$$" << e->toInteger(); #endif - } - } else { - insnTemplate << fmt << "<<" << (mode == Mode_Input ? "in" : "out") - << asmcode->args.size() << ">>"; - asmcode->args.push_back(AsmArg(type, e, mode)); } + } else { + insnTemplate << fmt << "<<" << (mode == Mode_Input ? "in" : "out") + << asmcode->args.size() << ">>"; + asmcode->args.push_back(AsmArg(type, e, mode)); } } void addOperand2(const char *fmtpre, const char *fmtpost, AsmArgType type, Expression *e, AsmCode *asmcode, AsmArgMode mode = Mode_Input) { - if (false && sc->func->isNaked()) { - // taken from above - error(stmt->loc, "only global variables can be referenced by identifier in " - "naked asm"); - return; - } - insnTemplate << fmtpre << "<<" << (mode == Mode_Input ? "in" : "out") << ">>" << fmtpost; asmcode->args.push_back(AsmArg(type, e, mode)); @@ -3090,11 +3029,9 @@ struct AsmProcessor { // If we subtract 8 from our const-displacement, then we can use the `H` modifier to ensure // that we always end up with a valid syntax for a memory operand with an offset. // So, we do just that when we have const-displacement in play. - // (Only for non-naked asm, as this isn't an issue for naked asm.) // // See also: https://lists.llvm.org/pipermail/llvm-dev/2017-August/116244.html - const auto forceLeadingDisplacement = hasConstDisplacement;// && !sc->func->isNaked(); - if (forceLeadingDisplacement) { + if (hasConstDisplacement) { // Subtract 8 from our const-displacement, and prepare to add the 8 from the `H` modifier. insnTemplate << "-8+"; } @@ -3104,14 +3041,11 @@ struct AsmProcessor { use_star = false; } - if (true || !sc->func->isNaked()) // no addrexp in naked asm please :) - { - Type *tt = pointerTo(e->type); - e = createAddrExp(Loc(), e); - e->type = tt; - } + Type *tt = pointerTo(e->type); + e = createAddrExp(Loc(), e); + e->type = tt; - if (forceLeadingDisplacement) { + if (hasConstDisplacement) { // We have a const-displacement in play, so we add the `H` modifier, as described earlier. insnTemplate << "${" << "<<" << (mode == Mode_Input ? "in" : "out") << asmcode->args.size() << ">>" << ":H}"; diff --git a/gen/naked.cpp b/gen/naked.cpp index 5a458c04704..e63ed83d557 100644 --- a/gen/naked.cpp +++ b/gen/naked.cpp @@ -16,17 +16,14 @@ #include "dmd/template.h" #include "gen/dvalue.h" #include "gen/funcgenstate.h" -#include "gen/functions.h" #include "gen/irstate.h" #include "gen/llvm.h" #include "gen/llvmhelpers.h" #include "gen/logger.h" -#include "gen/mangling.h" #include "gen/tollvm.h" #include "ir/irfunction.h" #include "llvm/IR/InlineAsm.h" #include -#include using namespace dmd; From 006e01bc2da44233478a2941dc30fc13ee7fd73a Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 7 Feb 2026 03:15:55 +0100 Subject: [PATCH 12/19] Naked DMD-style inline asm: Disallow branches to non-asm labels --- gen/asmstmt.cpp | 12 ++++++++++++ runtime/phobos | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/gen/asmstmt.cpp b/gen/asmstmt.cpp index 6a67efe08be..09ea1765546 100644 --- a/gen/asmstmt.cpp +++ b/gen/asmstmt.cpp @@ -544,6 +544,18 @@ void CompoundAsmStatement_toIR(CompoundAsmStatement *stmt, IRState *p) { continue; } + // the extra forwarding code needs an i32 alloca, so messes with the stack + if (fd->isNaked()) { + error(stmt->loc, + "branch to non-assembly D label `%s` not allowed in " + "naked inline assembly", + ident->toChars()); + errorSupplemental( + stmt->loc, + "Either define the label inside an `asm` block, or drop `naked`."); + continue; + } + // record that the jump needs to be handled in the post-asm dispatcher gotoToVal[targetLabel] = n_goto; diff --git a/runtime/phobos b/runtime/phobos index e1670433ff4..180145a02c0 160000 --- a/runtime/phobos +++ b/runtime/phobos @@ -1 +1 @@ -Subproject commit e1670433ff4e373e0afa9edbe8b534b8d40c4f63 +Subproject commit 180145a02c0b410e50448bd4cf6744e39508c318 From 19a56a5e18aa316f1212af792bb41bb8b4784f06 Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 7 Feb 2026 16:39:54 +0100 Subject: [PATCH 13/19] Alternative approach: Allow jumping to D labels in naked DMD-style inline-asm by making them asm labels too --- gen/asmstmt.cpp | 24 ++++++++---------------- gen/statements.cpp | 22 +++++++++++++++++----- runtime/phobos | 2 +- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/gen/asmstmt.cpp b/gen/asmstmt.cpp index 09ea1765546..b5c868a844c 100644 --- a/gen/asmstmt.cpp +++ b/gen/asmstmt.cpp @@ -532,10 +532,14 @@ void CompoundAsmStatement_toIR(CompoundAsmStatement *stmt, IRState *p) { } Identifier *const ident = targetLabel->ident; - // if the label is defined in inline asm, no special handling is necessary, skip - if (fd->asmLabels && llvm::any_of(*fd->asmLabels, [ident](Identifier *i) { - return i->equals(ident); - })) { + // if the label is defined in inline asm, we can jump to it directly + if (fd->isNaked() // in naked DMD-style inline-asm functions, all D labels + // become assembly labels too (and the extra forwarding + // code would mess with the stack due to an i32 alloca) + || (fd->asmLabels && + llvm::any_of(*fd->asmLabels, [ident](Identifier *i) { + return i->equals(ident); + }))) { continue; } @@ -544,18 +548,6 @@ void CompoundAsmStatement_toIR(CompoundAsmStatement *stmt, IRState *p) { continue; } - // the extra forwarding code needs an i32 alloca, so messes with the stack - if (fd->isNaked()) { - error(stmt->loc, - "branch to non-assembly D label `%s` not allowed in " - "naked inline assembly", - ident->toChars()); - errorSupplemental( - stmt->loc, - "Either define the label inside an `asm` block, or drop `naked`."); - continue; - } - // record that the jump needs to be handled in the post-asm dispatcher gotoToVal[targetLabel] = n_goto; diff --git a/gen/statements.cpp b/gen/statements.cpp index ab0fd31fe61..ecb583caae5 100644 --- a/gen/statements.cpp +++ b/gen/statements.cpp @@ -1542,15 +1542,20 @@ class ToIRVisitor : public Visitor { auto &PGO = irs->funcGen().pgo; PGO.setCurrentStmt(stmt); + auto fd = irs->func()->decl; + + const auto getFullAsmLabelString = [stmt, fd]() { + std::stringstream s; + printLabelName(s, mangleExact(fd), stmt->ident->toChars()); + s << ":"; + return s.str(); + }; + // if it's an inline asm label, we don't create a basicblock, just emit it // in the asm if (irs->asmBlock) { auto a = new IRAsmStmt; - std::stringstream label; - printLabelName(label, mangleExact(irs->func()->decl), - stmt->ident->toChars()); - label << ":"; - a->code = label.str(); + a->code = getFullAsmLabelString(); irs->asmBlock->s.push_back(a); // disable inlining @@ -1565,6 +1570,13 @@ class ToIRVisitor : public Visitor { } irs->ir->SetInsertPoint(labelBB); + + // in a naked DMD-style inline-asm function, create an assembly label too + // (so that inline-asm can jump to it directly) + if (fd->isNaked()) { + DtoInlineAsmExpr(stmt->loc, getFullAsmLabelString(), "", {}, {}, + irs->ir->getVoidTy()); + } } PGO.emitCounterIncrement(stmt); diff --git a/runtime/phobos b/runtime/phobos index 180145a02c0..e1670433ff4 160000 --- a/runtime/phobos +++ b/runtime/phobos @@ -1 +1 @@ -Subproject commit 180145a02c0b410e50448bd4cf6744e39508c318 +Subproject commit e1670433ff4e373e0afa9edbe8b534b8d40c4f63 From 7c970886a4b142a15847f1b232313a96b9144158 Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 4 Apr 2026 18:10:29 +0200 Subject: [PATCH 14/19] Remove 2 new naked DMD-style inline-asm tests again I don't think we need these 2 anymore, as they tested problems in the former separate `DtoDefineNakedFunction()` function. There is no separate codegen for these naked function anymore. --- tests/codegen/naked_lambda_no_dllexport.d | 30 ----------------- tests/linking/naked_lambda_linkage.d | 41 ----------------------- tests/lit.site.cfg.in | 4 --- 3 files changed, 75 deletions(-) delete mode 100644 tests/codegen/naked_lambda_no_dllexport.d delete mode 100644 tests/linking/naked_lambda_linkage.d diff --git a/tests/codegen/naked_lambda_no_dllexport.d b/tests/codegen/naked_lambda_no_dllexport.d deleted file mode 100644 index 5b086cfeb1a..00000000000 --- a/tests/codegen/naked_lambda_no_dllexport.d +++ /dev/null @@ -1,30 +0,0 @@ -// Tests that naked lambda functions don't get dllexport with internal linkage. -// -// Lambda functions get internal linkage, and internal linkage is incompatible -// with dllexport storage class. This caused an assertion failure: -// "local linkage requires DefaultStorageClass" -// -// REQUIRES: target_X86 - -// RUN: %ldc -mtriple=x86_64-windows-msvc -fvisibility=public -c %s --output-ll -of=%t.ll - -// The lambda should have internal linkage but NOT dllexport. -// Bug: "define internal dllexport" - invalid IR that fails LLVM verification. -// Use LLVM opt verifier to check for invalid IR. -// RUN: opt -passes=verify -S %t.ll -o /dev/null - -module naked_lambda_no_dllexport; - -void caller() { - // Function literal (lambda) with naked asm gets internal linkage. - // With -fvisibility=public, this must NOT get dllexport. - auto nakedLambda = () { - asm { - naked; - xor EAX, EAX; - ret; - } - }; - - nakedLambda(); -} diff --git a/tests/linking/naked_lambda_linkage.d b/tests/linking/naked_lambda_linkage.d deleted file mode 100644 index 030d941e9ec..00000000000 --- a/tests/linking/naked_lambda_linkage.d +++ /dev/null @@ -1,41 +0,0 @@ -// Tests that naked template functions get correct symbol mangling on Windows. -// -// The bug: When a naked template function is called, DtoDeclareFunction creates -// a declaration with the proper IR mangle name (including \01 prefix for -// vectorcall). Without the fix, DtoDefineNakedFunction would create a definition -// with a DIFFERENT mangle name (missing the \01 prefix), leaving the declared -// function undefined. -// -// This test verifies that calling and defining a naked template in the same -// module produces a linkable object file. -// -// See: https://github.com/ldc-developers/ldc/issues/4294 - -// REQUIRES: target_X86 -// REQUIRES: atleast_llvm1600 -// REQUIRES: ld.lld - -// Compile for Windows with LTO and link into a DLL. -// LTO is critical for this test - without LTO, the linker resolves both -// symbol variants to the same name, masking the bug. -// RUN: %ldc -mtriple=x86_64-windows-msvc -betterC -flto=full -c %s -of=%t.obj -// RUN: ld.lld -flavor link /dll /noentry /export:caller %t.obj /out:%t.dll - -module naked_lambda_linkage; - -// Non-exported naked template function. -// The call in caller() triggers DtoDeclareFunction first, then the function -// is defined by DtoDefineNakedFunction. Without the fix, these use different -// mangle names and the declared symbol remains undefined. -uint nakedTemplateFunc(int N)() pure @safe @nogc { - asm pure nothrow @nogc @trusted { - naked; - mov EAX, N; - ret; - } -} - -// This function calls the naked template, triggering the declaration before definition -extern(C) export uint caller() { - return nakedTemplateFunc!42(); -} diff --git a/tests/lit.site.cfg.in b/tests/lit.site.cfg.in index edcb29712fb..6fbfc1d3abd 100644 --- a/tests/lit.site.cfg.in +++ b/tests/lit.site.cfg.in @@ -214,10 +214,6 @@ if (platform.system() == 'Windows') and os.path.isfile( cdb ): if (platform.system() != 'Windows') and lit.util.which('gdb', config.environment['PATH']): config.available_features.add('gdb') -# Check whether ld.lld is present (for cross-platform COFF linking tests) -if lit.util.which('ld.lld', config.environment['PATH']): - config.available_features.add('ld.lld') - if 'LD_LIBRARY_PATH' in os.environ: libs = [] for lib_path in [s for s in os.environ['LD_LIBRARY_PATH'].split(':') if s]: From aec8193c2a2e00b6ee71c0a2d70703fbe725f0bb Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 4 Apr 2026 18:43:49 +0200 Subject: [PATCH 15/19] [revise new tests/codegen/naked_asm_output.d] --- tests/codegen/naked_asm_output.d | 93 ++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/tests/codegen/naked_asm_output.d b/tests/codegen/naked_asm_output.d index 4a5f6b84cc5..a08400e2bac 100644 --- a/tests/codegen/naked_asm_output.d +++ b/tests/codegen/naked_asm_output.d @@ -9,45 +9,46 @@ // REQUIRES: target_X86 +// Generate both .s (asm) and .ll (IR) outputs at once. +// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-s -output-ll -of=%t.s %s + // Test 1: Basic naked function - verify no prologue/epilogue -// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-s -of=%t.s %s // RUN: FileCheck %s --check-prefix=ASM < %t.s // Test 2: Verify LLVM IR has correct attributes -// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-ll -of=%t.ll %s // RUN: FileCheck %s --check-prefix=IR < %t.ll module naked_asm_output; // ASM-LABEL: simpleNaked: -// ASM-NOT: pushq %rbp -// ASM-NOT: movq %rsp, %rbp -// ASM: xorl %eax, %eax -// ASM: retq -// ASM-NOT: popq %rbp +// ASM-NEXT: .cfi_startproc +// ASM-NEXT: #APP +// ASM-NEXT: xorl %eax, %eax +// ASM-NEXT: retq // IR-LABEL: define i32 @simpleNaked() // IR-SAME: #[[ATTRS:[0-9]+]] extern(C) int simpleNaked() { - asm { naked; } asm { + naked; xor EAX, EAX; ret; } } // ASM-LABEL: nakedWithLabels: -// ASM-NOT: pushq %rbp -// ASM: xorl %eax, %eax -// ASM: .LnakedWithLabels_loop: -// ASM: incl %eax -// ASM: cmpl $10, %eax -// ASM: jl .LnakedWithLabels_loop -// ASM: retq +// ASM-NEXT: .cfi_startproc +// ASM-NEXT: #APP +// ASM-NEXT: xorl %eax, %eax +// ASM-NEXT: .LnakedWithLabels_loop: +// ASM-NEXT: incl %eax +// ASM-NEXT: cmpl $10, %eax +// ASM-NEXT: jl .LnakedWithLabels_loop +// ASM-NEXT: retq extern(C) int nakedWithLabels() { - asm { naked; } asm { + naked; xor EAX, EAX; loop: inc EAX; @@ -58,25 +59,39 @@ extern(C) int nakedWithLabels() { } // ASM-LABEL: nakedWithMultipleLabels: -// ASM-NOT: pushq %rbp -// ASM: .LnakedWithMultipleLabels_start: -// ASM: .LnakedWithMultipleLabels_middle: -// ASM: .LnakedWithMultipleLabels_end: -// ASM: retq +// ASM-NEXT: .cfi_startproc +// ASM-NEXT: #APP +// ASM-NEXT: jl .LnakedWithMultipleLabels_innerAsmLabel +// ASM-NEXT: .LnakedWithMultipleLabels_innerAsmLabel: +// ASM-NEXT: jl .LnakedWithMultipleLabels_otherAsmLabel +// ASM-NEXT: #NO_APP +// ASM-NEXT: #APP +// ASM-NEXT: .LnakedWithMultipleLabels_otherAsmLabel: +// ASM-NEXT: jl .LnakedWithMultipleLabels_dLabel +// ASM-NEXT: #NO_APP +// ASM-NEXT: #APP +// ASM-NEXT: .LnakedWithMultipleLabels_dLabel: +// ASM-NEXT: #NO_APP +// ASM-NEXT: #APP +// ASM-NEXT: jl .LnakedWithMultipleLabels_innerAsmLabel +// ASM-NEXT: jl .LnakedWithMultipleLabels_otherAsmLabel +// ASM-NEXT: retq extern(C) int nakedWithMultipleLabels() { - asm { naked; } asm { - xor EAX, EAX; - start: - inc EAX; - cmp EAX, 5; - jl start; - middle: - inc EAX; - cmp EAX, 10; - jl middle; - end: + naked; + jl innerAsmLabel; + innerAsmLabel: + jl otherAsmLabel; + } + asm { + otherAsmLabel: + jl dLabel; + } +dLabel: + asm { + jl innerAsmLabel; + jl otherAsmLabel; ret; } } @@ -84,17 +99,17 @@ extern(C) int nakedWithMultipleLabels() { // Template function - should have comdat for deduplication // ASM: .section .text._D16naked_asm_output__T13nakedTemplateVii42ZQvFZi,"axG",@progbits,_D16naked_asm_output__T13nakedTemplateVii42ZQvFZi,comdat // ASM-LABEL: _D16naked_asm_output__T13nakedTemplateVii42ZQvFZi: -// ASM-NOT: pushq %rbp -// ASM: movl $42, %eax -// ASM: retq +// ASM-NEXT: .cfi_startproc +// ASM-NEXT: #APP +// ASM-NEXT: movl $42, %eax +// ASM-NEXT: retq // Template function check - use IR-DAG to allow flexible ordering since // the template may be emitted after its caller (instantiate1) -// IR-DAG: define weak_odr i32 @_D16naked_asm_output__T13nakedTemplateVii42ZQvFZi(){{.*}}comdat - +// IR-DAG: define weak_odr i32 @_D16naked_asm_output__T13nakedTemplateVii42ZQvFZi() #[[ATTRS]] comdat int nakedTemplate(int N)() { - asm { naked; } asm { + naked; mov EAX, N; ret; } @@ -105,5 +120,5 @@ int instantiate1() { return nakedTemplate!42(); } -// Verify naked attribute is present in attributes group +// Verify naked and noinline attributes are present in attributes group // IR: attributes #[[ATTRS]] = {{{.*}}naked{{.*}}noinline From 325cad1fab83cf1e805450013c68c936b2db5381 Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 4 Apr 2026 19:31:48 +0200 Subject: [PATCH 16/19] [revise new tests/codegen/naked_asm_corner_cases.d] --- tests/codegen/naked_asm_corner_cases.d | 226 +++++++------------------ 1 file changed, 58 insertions(+), 168 deletions(-) diff --git a/tests/codegen/naked_asm_corner_cases.d b/tests/codegen/naked_asm_corner_cases.d index 5805d91d1ba..3bbd2a10c21 100644 --- a/tests/codegen/naked_asm_corner_cases.d +++ b/tests/codegen/naked_asm_corner_cases.d @@ -6,212 +6,102 @@ // 3. Nested labels // 4. Naked function calling convention -// REQUIRES: target_X86 -// REQUIRES: host_X86 +// REQUIRES: host_X86 && target_X86 -// FileCheck verification uses explicit triple for reproducible output -// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-s -of=%t.s %s -// RUN: FileCheck %s --check-prefix=ASM < %t.s - -// Runtime verification uses native platform (only on 64-bit) -// RUN: %ldc -O0 -run %s +// RUN: %ldc -run %s module naked_asm_corner_cases; // Test 1: Stack manipulation with push/pop -// ASM-LABEL: stackManipulation: -// ASM-NOT: pushq %rbp -// ASM-NOT: movq %rsp, %rbp -// ASM: pushq %rbx -// ASM: movl $42, %eax -// ASM: movl %eax, %ebx -// ASM: movl %ebx, %eax -// ASM: popq %rbx -// ASM: retq extern(C) int stackManipulation() { - version(D_InlineAsm_X86_64) { - asm { naked; } - asm { - push RBX; // Save callee-saved register - mov EAX, 42; - mov EBX, EAX; // Use the saved register - mov EAX, EBX; - pop RBX; // Restore - ret; - } - } - else version(D_InlineAsm_X86) { - asm { naked; } - asm { - push EBX; // Save callee-saved register - mov EAX, 42; - mov EBX, EAX; // Use the saved register - mov EAX, EBX; - pop EBX; // Restore - ret; - } + asm { naked; } + // Save callee-saved register + version (D_InlineAsm_X86_64) + asm { push RBX; } + else + asm { push EBX; } + asm { + mov EAX, 42; + mov EBX, EAX; // Use the saved register + mov EAX, EBX; } - else return 42; // Fallback for non-x86 + // Restore + version (D_InlineAsm_X86_64) + asm { pop RBX; } + else + asm { pop EBX; } + asm { ret; } } // Test 2: Forward jump (jump to label defined later) -// ASM-LABEL: forwardJump: -// ASM: jmp .LforwardJump_skip -// ASM: .LforwardJump_skip: -// ASM: retq extern(C) int forwardJump() { - version(D_InlineAsm_X86_64) { - asm { naked; } - asm { - mov EAX, 1; - jmp skip; // Forward jump - mov EAX, 0; // Should be skipped - skip: - ret; - } + asm { + naked; + mov EAX, 1; + jmp skip; // Forward jump + mov EAX, 0; // Should be skipped + skip: + ret; } - else version(D_InlineAsm_X86) { - asm { naked; } - asm { - mov EAX, 1; - jmp skip; - mov EAX, 0; - skip: - ret; - } - } - else return 1; } // Test 3: Backward jump (loop) -// ASM-LABEL: backwardJump: -// ASM: .LbackwardJump_again: -// ASM: incl %eax -// ASM: cmpl $5, %eax -// ASM: jl .LbackwardJump_again extern(C) int backwardJump() { - version(D_InlineAsm_X86_64) { - asm { naked; } - asm { - xor EAX, EAX; - again: - inc EAX; - cmp EAX, 5; - jl again; // Backward jump - ret; - } + asm { + naked; + xor EAX, EAX; + again: + inc EAX; + cmp EAX, 5; + jl again; // Backward jump + ret; } - else version(D_InlineAsm_X86) { - asm { naked; } - asm { - xor EAX, EAX; - again: - inc EAX; - cmp EAX, 5; - jl again; - ret; - } - } - else return 5; } // Test 4: Multiple control flow paths -// ASM-LABEL: multiPath: -// ASM: .LmultiPath_path1: -// ASM: .LmultiPath_path2: -// ASM: .LmultiPath_done: extern(C) int multiPath(int x) { - version(D_InlineAsm_X86_64) { - asm { naked; } - version(Windows) asm { - // x is in ECX on Windows x64 ABI - test ECX, ECX; - jz path1; - jmp path2; - path1: - mov EAX, 10; - jmp done; - path2: - mov EAX, 20; - done: - ret; - } - else asm { - // x is in EDI on SysV ABI - test EDI, EDI; - jz path1; - jmp path2; - path1: - mov EAX, 10; - jmp done; - path2: - mov EAX, 20; - done: - ret; - } - } - else version(D_InlineAsm_X86) { - asm { naked; } + asm { naked; } + version (D_InlineAsm_X86_64) { + // x is in ECX for the Win64 ABI, otherwise EDI for SysV + version (Windows) + asm { test ECX, ECX; } + else + asm { test EDI, EDI; } + } else { + // x is on stack at [ESP+4] for 32-bit cdecl asm { - // x is on stack at [ESP+4] for 32-bit cdecl mov EAX, [ESP+4]; test EAX, EAX; - jz path1; - jmp path2; - path1: - mov EAX, 10; - jmp done; - path2: - mov EAX, 20; - done: - ret; } } - else return x == 0 ? 10 : 20; + asm { + jz path1; + jmp path2; + path1: + mov EAX, 10; + jmp done; + path2: + mov EAX, 20; + done: + ret; + } } -// Test 5: Naked function with static variable declaration (triggers Declaration_codegen) -// This tests that static declarations inside naked functions work correctly. -// The visitor's ExpStatement::visit calls Declaration_codegen for these, -// which requires an active IR insert point. -// ASM-LABEL: nakedWithStaticDecl: -// ASM: movl $42, %eax -// ASM: retq +// Test 5: Naked function with static immutable variable declaration extern(C) int nakedWithStaticDecl() { - // Static variable declaration - triggers Declaration_codegen in visitor static immutable int staticVal = 42; - version(D_InlineAsm_X86_64) { - asm { naked; } - asm { - mov EAX, 42; // Use literal value since asm can't reference D variables - ret; - } + asm { + naked; + mov EAX, staticVal; + ret; } - else version(D_InlineAsm_X86) { - asm { naked; } - asm { - mov EAX, 42; - ret; - } - } - else return 42; } -// Test 6: Runtime verification void main() { - // Verify stack manipulation works assert(stackManipulation() == 42, "stackManipulation failed"); - - // Verify forward jump works assert(forwardJump() == 1, "forwardJump failed"); - - // Verify backward jump (loop) works assert(backwardJump() == 5, "backwardJump failed"); - - // Verify multi-path control flow assert(multiPath(0) == 10, "multiPath(0) failed"); assert(multiPath(1) == 20, "multiPath(1) failed"); - - // Verify naked function with static declaration works assert(nakedWithStaticDecl() == 42, "nakedWithStaticDecl failed"); } From 3a12831f250da06982f48676c6213e55d55983a5 Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 4 Apr 2026 19:41:33 +0200 Subject: [PATCH 17/19] [revise new tests/linking/asm_labels_lto.d] --- tests/linking/asm_labels_lto.d | 44 ++++++++++------------------------ 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/tests/linking/asm_labels_lto.d b/tests/linking/asm_labels_lto.d index dcfa1ba4b24..968b2ca322b 100644 --- a/tests/linking/asm_labels_lto.d +++ b/tests/linking/asm_labels_lto.d @@ -8,12 +8,11 @@ // // See: https://github.com/ldc-developers/ldc/issues/4294 -// REQUIRES: LTO -// REQUIRES: target_X86 +// REQUIRES: LTO && host_X86 && target_X86 // RUN: split-file %s %t -// Compile two modules that each instantiate the same naked asm template. +// Compile two modules (separately) that each instantiate the same naked asm template. // RUN: %ldc -flto=full -c -I%t %t/asm_lto_user.d -of=%t/user%obj // RUN: %ldc -flto=full -c -I%t %t/asm_lto_main.d -of=%t/main%obj @@ -23,41 +22,22 @@ // Verify the executable runs correctly. // RUN: %t/test%exe -//--- asm_lto_template.d +//--- asm_lto_template.di // Template with naked function containing inline asm labels. // This mimics std.internal.math.biguintx86 which triggers issue #4294. -// The naked function's asm becomes "module asm" which gets concatenated -// during LTO, causing duplicate symbol errors without the fix. module asm_lto_template; // Template function - when instantiated in multiple modules and linked // with LTO, the labels must have unique IDs to avoid "symbol already defined" uint nakedAsmTemplate(int N)() { - version (D_InlineAsm_X86) { - asm { naked; } - asm { - xor EAX, EAX; - L1: - add EAX, N; - cmp EAX, 100; - jl L1; - ret; - } - } else version (D_InlineAsm_X86_64) { - asm { naked; } - asm { - xor EAX, EAX; - L1: - add EAX, N; - cmp EAX, 100; - jl L1; - ret; - } - } else { - // Fallback for non-x86 - uint result = 0; - while (result < 100) result += N; - return result; + asm { + naked; + xor EAX, EAX; + L1: + add EAX, N; + cmp EAX, 100; + jl L1; + ret; } } @@ -84,7 +64,7 @@ int main() { // Both modules instantiate nakedAsmTemplate!1 // Without unique label IDs, LTO linking fails with "symbol already defined" uint a = nakedAsmTemplate!1(); // From this module's instantiation - uint b = useTemplate(); // From asm_lto_user's instantiation + uint b = useTemplate(); // From asm_lto_user's instantiation // Both should return the same value (>= 100) return (a == b && a >= 100) ? 0 : 1; From c98dc836819868793f104a4742f2f4939af77aba Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 4 Apr 2026 20:35:11 +0200 Subject: [PATCH 18/19] [add changelog entry] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 701c537c3f9..d9f19defa1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ #### Big news - Support for [LLVM 22](https://releases.llvm.org/22.1.0/docs/ReleaseNotes.html). The prebuilt packages use v22.1.2. (#5097, #5102) - Minimum LLVM version raised to 18. (#5094) +- DMD-style inline assembly: `asm { naked; }` is now much less of a special case wrt. codegen - such naked functions are now emitted as if `asm { naked; }` was replaced with the `@naked` function UDA. IR-wise, they are not emitted as module-level asm blobs anymore, but regular IR functions. This should lift a few former `asm { naked; }`-specific restrictions and fixed at least one LTO issue. (#5041) - Predefined version `LDC_LLVM_*` now only contains the LLVM major version, i.e., former `version (LDC_LLVM_1801)` with LLVM v18.1 is now `version (LDC_LLVM_18)`. Use `ldc.intrinsics.LLVM_version` for backwards compatibility if really needed. (#5109) #### Platform support From 3206d530fee5bf5b54e2166358b4d8c6d3cc6bda Mon Sep 17 00:00:00 2001 From: Martin Kinkelin Date: Sat, 4 Apr 2026 21:06:11 +0200 Subject: [PATCH 19/19] [tiny nits after last review pass] --- dmd/declaration.h | 2 +- tests/codegen/naked_asm_output.d | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dmd/declaration.h b/dmd/declaration.h index 6f94b76b54a..5c4562aab90 100644 --- a/dmd/declaration.h +++ b/dmd/declaration.h @@ -582,7 +582,7 @@ class FuncDeclaration : public Declaration VarDeclarations *parameters; // Array of VarDeclaration's for parameters DsymbolTable *labtab; // statement label symbol table #if IN_LLVM - Identifiers *asmLabels; + Identifiers *asmLabels; // identifiers of labels defined in DMD-style inline assembly #endif Dsymbol *overnext; // next in overload list FuncDeclaration *overnext0; // next in overload list (only used during IFTI) diff --git a/tests/codegen/naked_asm_output.d b/tests/codegen/naked_asm_output.d index a08400e2bac..c7e6d8d1adc 100644 --- a/tests/codegen/naked_asm_output.d +++ b/tests/codegen/naked_asm_output.d @@ -61,37 +61,37 @@ extern(C) int nakedWithLabels() { // ASM-LABEL: nakedWithMultipleLabels: // ASM-NEXT: .cfi_startproc // ASM-NEXT: #APP -// ASM-NEXT: jl .LnakedWithMultipleLabels_innerAsmLabel +// ASM-NEXT: jmp .LnakedWithMultipleLabels_innerAsmLabel // ASM-NEXT: .LnakedWithMultipleLabels_innerAsmLabel: -// ASM-NEXT: jl .LnakedWithMultipleLabels_otherAsmLabel +// ASM-NEXT: jmp .LnakedWithMultipleLabels_otherAsmLabel // ASM-NEXT: #NO_APP // ASM-NEXT: #APP // ASM-NEXT: .LnakedWithMultipleLabels_otherAsmLabel: -// ASM-NEXT: jl .LnakedWithMultipleLabels_dLabel +// ASM-NEXT: jmp .LnakedWithMultipleLabels_dLabel // ASM-NEXT: #NO_APP // ASM-NEXT: #APP // ASM-NEXT: .LnakedWithMultipleLabels_dLabel: // ASM-NEXT: #NO_APP // ASM-NEXT: #APP -// ASM-NEXT: jl .LnakedWithMultipleLabels_innerAsmLabel -// ASM-NEXT: jl .LnakedWithMultipleLabels_otherAsmLabel +// ASM-NEXT: jmp .LnakedWithMultipleLabels_innerAsmLabel +// ASM-NEXT: jmp .LnakedWithMultipleLabels_otherAsmLabel // ASM-NEXT: retq extern(C) int nakedWithMultipleLabels() { asm { naked; - jl innerAsmLabel; + jmp innerAsmLabel; innerAsmLabel: - jl otherAsmLabel; + jmp otherAsmLabel; } asm { otherAsmLabel: - jl dLabel; + jmp dLabel; } dLabel: asm { - jl innerAsmLabel; - jl otherAsmLabel; + jmp innerAsmLabel; + jmp otherAsmLabel; ret; } }