From 3fc63121b0440dbc57497a72b6c746babce1d1ba Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Wed, 7 Oct 2026 13:13:11 +0800 Subject: [PATCH 1/2] llvm: expose function cloning and local IR transformation utilities --- transforms.cpp | 59 ++++++++++++++++++ transforms.go | 106 ++++++++++++++++++++++++++++++++ transforms.h | 33 ++++++++++ transforms_test.go | 149 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 347 insertions(+) create mode 100644 transforms.cpp create mode 100644 transforms.go create mode 100644 transforms.h create mode 100644 transforms_test.go diff --git a/transforms.cpp b/transforms.cpp new file mode 100644 index 0000000..ed0642a --- /dev/null +++ b/transforms.cpp @@ -0,0 +1,59 @@ +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +// Generic LLVM utilities exposed to Go; policy belongs to the caller. +#include "transforms.h" +#include "llvm/IR/DebugInfoMetadata.h" +#include "llvm/IR/InlineAsm.h" +#include "llvm/IR/Instructions.h" +#include "llvm/IR/Module.h" +#include "llvm/Transforms/Utils/Cloning.h" +#include "llvm/Transforms/Utils/Local.h" +using namespace llvm; + +LLVMValueRef LLVMGoGetNamedValue(LLVMModuleRef M, const char *Name, + size_t Len) { + return wrap(unwrap(M)->getNamedValue(StringRef(Name, Len))); +} +LLVMValueRef LLVMGoCloneFunction(LLVMValueRef Fn) { + ValueToValueMapTy Map; + return wrap(CloneFunction(unwrap(Fn), Map)); +} +LLVMBool LLVMGoSimplifyInstructionsInBlock(LLVMBasicBlockRef BB) { + return SimplifyInstructionsInBlock(unwrap(BB)); +} +LLVMBool LLVMGoConstantFoldTerminator(LLVMBasicBlockRef BB, + LLVMBool DeleteDead) { + return ConstantFoldTerminator(unwrap(BB), DeleteDead); +} +LLVMBool LLVMGoRemoveUnreachableBlocks(LLVMValueRef Fn) { + return removeUnreachableBlocks(*unwrap(Fn)); +} +void LLVMGoSetSubprogramLinkageName(LLVMMetadataRef SP, const char *Name, + size_t Len) { + auto *Subprogram = unwrap(SP); + Subprogram->replaceLinkageName( + MDString::get(Subprogram->getContext(), StringRef(Name, Len))); +} +void LLVMGoSetTailCallKind(LLVMValueRef Call, unsigned Kind) { + unwrap(Call)->setTailCallKind( + static_cast(Kind)); +} +unsigned LLVMGoGetTailCallKind(LLVMValueRef Call) { + return unwrap(Call)->getTailCallKind(); +} +LLVMGoInlineAsmInfo LLVMGoGetInlineAsmInfo(LLVMValueRef Asm) { + auto *A = unwrap(Asm); + const auto &Text = A->getAsmString(); + const auto &Constraints = A->getConstraintString(); + return {wrap(A->getFunctionType()), + Text.data(), + Constraints.data(), + Text.size(), + Constraints.size(), + A->hasSideEffects(), + A->isAlignStack(), + A->canThrow(), + static_cast(A->getDialect())}; +} diff --git a/transforms.go b/transforms.go new file mode 100644 index 0000000..89609ee --- /dev/null +++ b/transforms.go @@ -0,0 +1,106 @@ +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +package llvm + +/* +#include "transforms.h" +#include +*/ +import "C" +import "unsafe" + +// CloneFunction copies a function into its module, including its attributes, +// blocks, instructions and debug information. Local references are remapped; +// references to other functions and globals remain unchanged. The caller can +// rename the returned function. The original function is not modified. +func (v Value) CloneFunction() (clone Value) { + clone.C = C.LLVMGoCloneFunction(v.C) + return +} + +// SimplifyInstructionsInBlock folds instructions using locally available facts. +// It returns whether the block changed and may erase instructions. +func (bb BasicBlock) SimplifyInstructionsInBlock() bool { + return C.LLVMGoSimplifyInstructionsInBlock(bb.C) != 0 +} + +// ConstantFoldTerminator folds a constant branch or switch and updates successor +// PHIs. When deleteDead is true, it also removes dead condition instructions. +func (bb BasicBlock) ConstantFoldTerminator(deleteDead bool) bool { + return C.LLVMGoConstantFoldTerminator(bb.C, boolToLLVMBool(deleteDead)) != 0 +} + +// RemoveUnreachableBlocks deletes blocks unreachable from the function entry +// and updates successor PHIs. It returns whether the function changed. +func (v Value) RemoveUnreachableBlocks() bool { + return C.LLVMGoRemoveUnreachableBlocks(v.C) != 0 +} + +// SetSubprogramLinkageName changes the linker name of a DISubprogram without +// changing its source-language display name. md must be a DISubprogram. +func (md Metadata) SetSubprogramLinkageName(name string) { + text := C.CString(name) + defer C.free(unsafe.Pointer(text)) + C.LLVMGoSetSubprogramLinkageName(md.C, text, C.size_t(len(name))) +} + +// AsMetadata converts a constant or MetadataAsValue back into metadata. +func (v Value) AsMetadata() (md Metadata) { + md.C = C.LLVMValueAsMetadata(v.C) + return +} + +// TailCallKind distinguishes optional tail-call hints from mandatory tail calls. +type TailCallKind uint32 + +const ( + TailCallKindNone TailCallKind = iota + TailCallKindTail + TailCallKindMustTail + TailCallKindNoTail +) + +// TailCallKind returns the tail-call kind of a call instruction. +func (v Value) TailCallKind() TailCallKind { + return TailCallKind(C.LLVMGoGetTailCallKind(v.C)) +} + +// SetTailCallKind sets the tail-call kind of a call instruction. The caller must +// satisfy LLVM's ABI and control-flow requirements for mandatory tail calls. +func (v Value) SetTailCallKind(kind TailCallKind) { + C.LLVMGoSetTailCallKind(v.C, C.unsigned(kind)) +} + +// InlineAsmInfo contains all properties needed to rebuild an inline assembly +// value while changing its text or constraints. +type InlineAsmInfo struct { + Type Type + Assembly, Constraints string + HasSideEffects, IsAlignStack, CanThrow bool + Dialect InlineAsmDialect +} + +// InlineAsmInfo returns the properties of an inline assembly value. +func (v Value) InlineAsmInfo() InlineAsmInfo { + a := C.LLVMGoGetInlineAsmInfo(v.C) + return InlineAsmInfo{ + Type: Type{C: a.Type}, + Assembly: C.GoStringN(a.Assembly, C.int(a.AssemblyLen)), + Constraints: C.GoStringN(a.Constraints, C.int(a.ConstraintsLen)), + HasSideEffects: a.SideEffects != 0, + IsAlignStack: a.AlignStack != 0, + CanThrow: a.CanThrow != 0, + Dialect: InlineAsmDialect(a.Dialect), + } +} + +// NamedValue looks up a symbol of any global kind, including functions, global +// variables, aliases and indirect functions. It returns a nil Value if absent. +func (m Module) NamedValue(name string) (value Value) { + text := C.CString(name) + defer C.free(unsafe.Pointer(text)) + value.C = C.LLVMGoGetNamedValue(m.C, text, C.size_t(len(name))) + return +} diff --git a/transforms.h b/transforms.h new file mode 100644 index 0000000..4adeb7b --- /dev/null +++ b/transforms.h @@ -0,0 +1,33 @@ +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +// Generic IR transformation utilities missing from llvm-c. +#ifndef LLVM_BINDINGS_GO_TRANSFORMS_H +#define LLVM_BINDINGS_GO_TRANSFORMS_H +#include "llvm-c/Core.h" +#ifdef __cplusplus +extern "C" { +#endif +LLVMValueRef LLVMGoGetNamedValue(LLVMModuleRef M, const char *Name, size_t Len); +LLVMValueRef LLVMGoCloneFunction(LLVMValueRef Fn); +LLVMBool LLVMGoSimplifyInstructionsInBlock(LLVMBasicBlockRef BB); +LLVMBool LLVMGoConstantFoldTerminator(LLVMBasicBlockRef BB, + LLVMBool DeleteDead); +LLVMBool LLVMGoRemoveUnreachableBlocks(LLVMValueRef Fn); +void LLVMGoSetSubprogramLinkageName(LLVMMetadataRef SP, const char *Name, + size_t Len); +void LLVMGoSetTailCallKind(LLVMValueRef Call, unsigned Kind); +unsigned LLVMGoGetTailCallKind(LLVMValueRef Call); +struct LLVMGoInlineAsmInfo { + LLVMTypeRef Type; + const char *Assembly, *Constraints; + size_t AssemblyLen, ConstraintsLen; + LLVMBool SideEffects, AlignStack, CanThrow; + LLVMInlineAsmDialect Dialect; +}; +struct LLVMGoInlineAsmInfo LLVMGoGetInlineAsmInfo(LLVMValueRef Asm); +#ifdef __cplusplus +} +#endif +#endif diff --git a/transforms_test.go b/transforms_test.go new file mode 100644 index 0000000..6504a03 --- /dev/null +++ b/transforms_test.go @@ -0,0 +1,149 @@ +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +package llvm + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func transformationModule(t *testing.T, source string) Module { + t.Helper() + ctx := NewContext() + t.Cleanup(ctx.Dispose) + path := filepath.Join(t.TempDir(), "transforms.ll") + if err := os.WriteFile(path, []byte(source), 0600); err != nil { + t.Fatal(err) + } + buf, err := NewMemoryBufferFromFile(path) + if err != nil { + t.Fatal(err) + } + mod, err := ctx.ParseIR(buf) + if err != nil { + t.Fatal(err) + } + t.Cleanup(mod.Dispose) + return mod +} + +func TestCloneFunctionAndLocalCleanup(t *testing.T) { + mod := transformationModule(t, ` +declare i32 @external(i32) "test.attribute"="kept" +@address = global i32 0 +@alias = alias i32, i32* @address +define i32 @original(i32 %n, i1 %flag) noinline !dbg !3 { +entry: + %not = xor i1 %flag, true, !dbg !4 + br i1 %not, label %fallback, label %fast, !dbg !4 +fast: + %value = call i32 @external(i32 %n), !dbg !4 + br label %merge +fallback: + br label %merge +merge: + %result = phi i32 [ %value, %fast ], [ 0, %fallback ] + ret i32 %result, !dbg !4 +} +!llvm.dbg.cu = !{!0} +!llvm.module.flags = !{!5} +!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "test", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug) +!1 = !DIFile(filename: "source.go", directory: "/test") +!2 = !DISubroutineType(types: !6) +!3 = distinct !DISubprogram(name: "source.name", linkageName: "original", scope: !1, file: !1, line: 10, type: !2, scopeLine: 10, spFlags: DISPFlagDefinition, unit: !0) +!4 = !DILocation(line: 11, column: 1, scope: !3) +!5 = !{i32 2, !"Debug Info Version", i32 3} +!6 = !{} +`) + original := mod.NamedFunction("original") + before := original.String() + clone := original.CloneFunction() + clone.SetName("copy") + if clone.Subprogram() == original.Subprogram() || clone.Subprogram().IsNil() { + t.Fatal("clone must own distinct debug information") + } + clone.Subprogram().SetSubprogramLinkageName("copy") + if !strings.Contains(mod.String(), `linkageName: "copy"`) || !strings.Contains(mod.String(), `linkageName: "original"`) { + t.Fatal("debug linker names are not independent") + } + clone.Param(1).ReplaceAllUsesWith(ConstInt(mod.Context().Int1Type(), 1, false)) + for { + changed := false + for bb := clone.FirstBasicBlock(); !bb.IsNil(); bb = NextBasicBlock(bb) { + changed = bb.SimplifyInstructionsInBlock() || changed + changed = bb.ConstantFoldTerminator(true) || changed + } + changed = clone.RemoveUnreachableBlocks() || changed + if !changed { + break + } + } + if original.String() != before || strings.Contains(clone.String(), "fallback") || strings.Contains(clone.String(), "phi ") { + t.Fatalf("clone cleanup changed original or retained dead control flow:\n%s", clone.String()) + } + if !strings.Contains(clone.String(), "@external(") || clone.GetEnumFunctionAttribute(AttributeKindID("noinline")).IsNil() { + t.Fatal("external references or attributes were lost") + } + decl := mod.NamedFunction("external").CloneFunction() + decl.SetName("external.copy") + if !decl.IsDeclaration() || decl.GetStringAttributeAtIndex(-1, "test.attribute").GetStringValue() != "kept" { + t.Fatal("declaration clone lost its attributes") + } + for _, name := range []string{"original", "copy", "external", "address", "alias"} { + if mod.NamedValue(name).IsNil() { + t.Errorf("missing named value %s", name) + } + } + if !mod.NamedValue("absent").IsNil() { + t.Fatal("unknown symbol found") + } + if err := VerifyModule(mod, ReturnStatusAction); err != nil { + t.Fatal(err) + } +} + +func TestMustTailCallKinds(t *testing.T) { + mod := transformationModule(t, ` +declare i32 @callee(i32) +define i32 @forward(i32 %x) { + %r = call i32 @callee(i32 %x) + ret i32 %r +} +`) + call := mod.NamedFunction("forward").FirstBasicBlock().FirstInstruction() + for _, kind := range []TailCallKind{TailCallKindTail, TailCallKindNoTail, TailCallKindNone, TailCallKindMustTail} { + call.SetTailCallKind(kind) + if call.TailCallKind() != kind { + t.Fatalf("tail kind did not round trip: %d", kind) + } + if err := VerifyModule(mod, ReturnStatusAction); err != nil { + t.Fatal(err) + } + } + if !strings.Contains(call.String(), "musttail call") { + t.Fatal("musttail was downgraded to an optional hint") + } +} + +func TestInlineAsmInfoAndMetadataConversion(t *testing.T) { + ctx := NewContext() + defer ctx.Dispose() + typ := FunctionType(ctx.VoidType(), []Type{ctx.Int32Type()}, false) + for _, sideEffects := range []bool{false, true} { + asm := InlineAsm(typ, "# marker $0", "r", sideEffects, true, InlineAsmDialectIntel, true) + info := asm.InlineAsmInfo() + rebuilt := InlineAsm(info.Type, info.Assembly, info.Constraints, info.HasSideEffects, info.IsAlignStack, info.Dialect, info.CanThrow) + if rebuilt != asm || info.HasSideEffects != sideEffects || !info.IsAlignStack || !info.CanThrow || info.Dialect != InlineAsmDialectIntel { + t.Fatalf("inline asm properties changed: %+v", info) + } + } + for _, md := range []Metadata{ctx.MDString("name"), ctx.MDNode(nil), ConstInt(ctx.Int32Type(), 7, false).ConstantAsMetadata()} { + if ctx.MetadataAsValue(md).AsMetadata() != md { + t.Fatal("metadata conversion changed identity") + } + } +} From f6e797edc078026ccb6313c4c8a9c50ea71434c0 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 8 Oct 2026 19:01:33 +0800 Subject: [PATCH 2/2] bindings: validate transformation inputs and clarify API contracts --- transforms.cpp | 48 +++++++++++++----- transforms.go | 38 +++++++++++--- transforms.h | 13 +++-- transforms_test.go | 124 ++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 186 insertions(+), 37 deletions(-) diff --git a/transforms.cpp b/transforms.cpp index ed0642a..872ce69 100644 --- a/transforms.cpp +++ b/transforms.cpp @@ -12,13 +12,21 @@ #include "llvm/Transforms/Utils/Local.h" using namespace llvm; +// Keep the Go TailCallKind constants and the C boundary in sync with LLVM. +static_assert(CallInst::TCK_None == 0 && CallInst::TCK_Tail == 1 && + CallInst::TCK_MustTail == 2 && CallInst::TCK_NoTail == 3, + "update the Go TailCallKind constants for this LLVM version"); + LLVMValueRef LLVMGoGetNamedValue(LLVMModuleRef M, const char *Name, size_t Len) { return wrap(unwrap(M)->getNamedValue(StringRef(Name, Len))); } LLVMValueRef LLVMGoCloneFunction(LLVMValueRef Fn) { + auto *F = Fn ? dyn_cast(unwrap(Fn)) : nullptr; + if (!F) + return nullptr; ValueToValueMapTy Map; - return wrap(CloneFunction(unwrap(Fn), Map)); + return wrap(CloneFunction(F, Map)); } LLVMBool LLVMGoSimplifyInstructionsInBlock(LLVMBasicBlockRef BB) { return SimplifyInstructionsInBlock(unwrap(BB)); @@ -27,26 +35,40 @@ LLVMBool LLVMGoConstantFoldTerminator(LLVMBasicBlockRef BB, LLVMBool DeleteDead) { return ConstantFoldTerminator(unwrap(BB), DeleteDead); } -LLVMBool LLVMGoRemoveUnreachableBlocks(LLVMValueRef Fn) { - return removeUnreachableBlocks(*unwrap(Fn)); +int LLVMGoRemoveUnreachableBlocks(LLVMValueRef Fn) { + auto *F = Fn ? dyn_cast(unwrap(Fn)) : nullptr; + if (!F || F->isDeclaration()) + return -1; + return removeUnreachableBlocks(*F); } -void LLVMGoSetSubprogramLinkageName(LLVMMetadataRef SP, const char *Name, - size_t Len) { - auto *Subprogram = unwrap(SP); +LLVMBool LLVMGoSetSubprogramLinkageName(LLVMMetadataRef SP, const char *Name, + size_t Len) { + auto *Subprogram = SP ? dyn_cast(unwrap(SP)) : nullptr; + if (!Subprogram) + return false; Subprogram->replaceLinkageName( MDString::get(Subprogram->getContext(), StringRef(Name, Len))); + return true; } -void LLVMGoSetTailCallKind(LLVMValueRef Call, unsigned Kind) { - unwrap(Call)->setTailCallKind( - static_cast(Kind)); +LLVMBool LLVMGoSetTailCallKind(LLVMValueRef Call, unsigned Kind) { + auto *CI = Call ? dyn_cast(unwrap(Call)) : nullptr; + if (!CI || Kind > CallInst::TCK_NoTail) + return false; + CI->setTailCallKind(static_cast(Kind)); + return true; } unsigned LLVMGoGetTailCallKind(LLVMValueRef Call) { - return unwrap(Call)->getTailCallKind(); + auto *CI = Call ? dyn_cast(unwrap(Call)) : nullptr; + return CI ? CI->getTailCallKind() : ~0u; } LLVMGoInlineAsmInfo LLVMGoGetInlineAsmInfo(LLVMValueRef Asm) { - auto *A = unwrap(Asm); - const auto &Text = A->getAsmString(); - const auto &Constraints = A->getConstraintString(); + auto *A = Asm ? dyn_cast(unwrap(Asm)) : nullptr; + if (!A) + return {}; + // Borrow InlineAsm's storage with either the older std::string& API or the + // newer StringRef API. Copying a std::string here would return dangling data. + StringRef Text = A->getAsmString(); + StringRef Constraints = A->getConstraintString(); return {wrap(A->getFunctionType()), Text.data(), Constraints.data(), diff --git a/transforms.go b/transforms.go index 89609ee..d45fdb1 100644 --- a/transforms.go +++ b/transforms.go @@ -15,8 +15,12 @@ import "unsafe" // blocks, instructions and debug information. Local references are remapped; // references to other functions and globals remain unchanged. The caller can // rename the returned function. The original function is not modified. +// It panics if v is nil or is not a function; declarations may be cloned. func (v Value) CloneFunction() (clone Value) { clone.C = C.LLVMGoCloneFunction(v.C) + if clone.IsNil() { + panic("llvm: CloneFunction requires a function") + } return } @@ -34,19 +38,28 @@ func (bb BasicBlock) ConstantFoldTerminator(deleteDead bool) bool { // RemoveUnreachableBlocks deletes blocks unreachable from the function entry // and updates successor PHIs. It returns whether the function changed. +// It panics if v is nil or is not a function definition. func (v Value) RemoveUnreachableBlocks() bool { - return C.LLVMGoRemoveUnreachableBlocks(v.C) != 0 + changed := C.LLVMGoRemoveUnreachableBlocks(v.C) + if changed < 0 { + panic("llvm: RemoveUnreachableBlocks requires a function definition") + } + return changed != 0 } // SetSubprogramLinkageName changes the linker name of a DISubprogram without -// changing its source-language display name. md must be a DISubprogram. +// changing its source-language display name. It panics if md is nil or is not +// a DISubprogram. func (md Metadata) SetSubprogramLinkageName(name string) { text := C.CString(name) defer C.free(unsafe.Pointer(text)) - C.LLVMGoSetSubprogramLinkageName(md.C, text, C.size_t(len(name))) + if C.LLVMGoSetSubprogramLinkageName(md.C, text, C.size_t(len(name))) == 0 { + panic("llvm: SetSubprogramLinkageName requires a DISubprogram") + } } -// AsMetadata converts a constant or MetadataAsValue back into metadata. +// AsMetadata unwraps a MetadataAsValue, or wraps a constant, argument, or +// instruction as metadata using LLVMValueAsMetadata. func (v Value) AsMetadata() (md Metadata) { md.C = C.LLVMValueAsMetadata(v.C) return @@ -56,6 +69,7 @@ func (v Value) AsMetadata() (md Metadata) { type TailCallKind uint32 const ( + // These values match llvm::CallInst::TailCallKind, checked in transforms.cpp. TailCallKindNone TailCallKind = iota TailCallKindTail TailCallKindMustTail @@ -63,14 +77,22 @@ const ( ) // TailCallKind returns the tail-call kind of a call instruction. +// It panics if v is nil or is not a call instruction. func (v Value) TailCallKind() TailCallKind { - return TailCallKind(C.LLVMGoGetTailCallKind(v.C)) + kind := TailCallKind(C.LLVMGoGetTailCallKind(v.C)) + if kind > TailCallKindNoTail { + panic("llvm: TailCallKind requires a call instruction") + } + return kind } // SetTailCallKind sets the tail-call kind of a call instruction. The caller must // satisfy LLVM's ABI and control-flow requirements for mandatory tail calls. +// It panics if v is nil, is not a call instruction, or kind is invalid. func (v Value) SetTailCallKind(kind TailCallKind) { - C.LLVMGoSetTailCallKind(v.C, C.unsigned(kind)) + if C.LLVMGoSetTailCallKind(v.C, C.unsigned(kind)) == 0 { + panic("llvm: SetTailCallKind requires a call instruction and a valid tail-call kind") + } } // InlineAsmInfo contains all properties needed to rebuild an inline assembly @@ -83,8 +105,12 @@ type InlineAsmInfo struct { } // InlineAsmInfo returns the properties of an inline assembly value. +// It panics if v is nil or is not an inline assembly value. func (v Value) InlineAsmInfo() InlineAsmInfo { a := C.LLVMGoGetInlineAsmInfo(v.C) + if a.Type == nil { + panic("llvm: InlineAsmInfo requires an inline assembly value") + } return InlineAsmInfo{ Type: Type{C: a.Type}, Assembly: C.GoStringN(a.Assembly, C.int(a.AssemblyLen)), diff --git a/transforms.h b/transforms.h index 4adeb7b..32e4027 100644 --- a/transforms.h +++ b/transforms.h @@ -10,14 +10,17 @@ extern "C" { #endif LLVMValueRef LLVMGoGetNamedValue(LLVMModuleRef M, const char *Name, size_t Len); -LLVMValueRef LLVMGoCloneFunction(LLVMValueRef Fn); +// Invalid inputs are reported to the Go layer, which raises a Go panic. +LLVMValueRef LLVMGoCloneFunction(LLVMValueRef Fn); // null on invalid input LLVMBool LLVMGoSimplifyInstructionsInBlock(LLVMBasicBlockRef BB); LLVMBool LLVMGoConstantFoldTerminator(LLVMBasicBlockRef BB, LLVMBool DeleteDead); -LLVMBool LLVMGoRemoveUnreachableBlocks(LLVMValueRef Fn); -void LLVMGoSetSubprogramLinkageName(LLVMMetadataRef SP, const char *Name, - size_t Len); -void LLVMGoSetTailCallKind(LLVMValueRef Call, unsigned Kind); +// Returns -1 on invalid input, otherwise whether the function changed. +int LLVMGoRemoveUnreachableBlocks(LLVMValueRef Fn); +LLVMBool LLVMGoSetSubprogramLinkageName(LLVMMetadataRef SP, const char *Name, + size_t Len); +LLVMBool LLVMGoSetTailCallKind(LLVMValueRef Call, unsigned Kind); +// Returns ~0u on invalid input. unsigned LLVMGoGetTailCallKind(LLVMValueRef Call); struct LLVMGoInlineAsmInfo { LLVMTypeRef Type; diff --git a/transforms_test.go b/transforms_test.go index 6504a03..e8b6e72 100644 --- a/transforms_test.go +++ b/transforms_test.go @@ -6,11 +6,74 @@ package llvm import ( "os" + "os/exec" "path/filepath" "strings" "testing" ) +func TestTransformInvalidInputs(t *testing.T) { + // Keep a regression in the C boundary from aborting the entire test suite. + const childEnv = "LLVM_TRANSFORM_INVALID_INPUT_CHILD" + if os.Getenv(childEnv) != "1" { + cmd := exec.Command(os.Args[0], "-test.run=^TestTransformInvalidInputs$") + cmd.Env = append(os.Environ(), childEnv+"=1") + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("invalid inputs must produce recoverable Go panics: %v\n%s", err, out) + } + return + } + mod := transformationModule(t, ` +declare void @external() +define void @caller() { + call void @external() + ret void +} +`) + fn := mod.NamedFunction("caller") + call := fn.FirstBasicBlock().FirstInstruction() + integer := ConstInt(mod.Context().Int32Type(), 1, false) + cases := []struct { + name string + run func() + }{ + {"CloneFunction/nil", func() { Value{}.CloneFunction() }}, + {"CloneFunction/wrong-kind", func() { integer.CloneFunction() }}, + {"RemoveUnreachableBlocks/nil", func() { Value{}.RemoveUnreachableBlocks() }}, + {"RemoveUnreachableBlocks/wrong-kind", func() { integer.RemoveUnreachableBlocks() }}, + {"RemoveUnreachableBlocks/declaration", func() { mod.NamedFunction("external").RemoveUnreachableBlocks() }}, + {"SetSubprogramLinkageName/nil", func() { Metadata{}.SetSubprogramLinkageName("bad") }}, + {"SetSubprogramLinkageName/wrong-kind", func() { mod.Context().MDString("x").SetSubprogramLinkageName("bad") }}, + {"TailCallKind/nil", func() { Value{}.TailCallKind() }}, + {"TailCallKind/wrong-kind", func() { fn.TailCallKind() }}, + {"SetTailCallKind/nil", func() { Value{}.SetTailCallKind(TailCallKindTail) }}, + {"SetTailCallKind/wrong-kind", func() { fn.SetTailCallKind(TailCallKindTail) }}, + {"SetTailCallKind/invalid-enum", func() { call.SetTailCallKind(TailCallKind(4)) }}, + {"SetTailCallKind/large-enum", func() { call.SetTailCallKind(^TailCallKind(0)) }}, + {"InlineAsmInfo/nil", func() { Value{}.InlineAsmInfo() }}, + {"InlineAsmInfo/wrong-kind", func() { fn.InlineAsmInfo() }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + defer func() { + got := recover() + message, ok := got.(string) + method, _, _ := strings.Cut(tc.name, "/") + if !ok || !strings.Contains(message, method) { + t.Errorf("expected descriptive %s panic, got %v", method, got) + } + }() + tc.run() + }) + } + if call.TailCallKind() != TailCallKindNone { + t.Fatal("invalid tail kind changed the call") + } + if err := VerifyModule(mod, ReturnStatusAction); err != nil { + t.Fatal(err) + } +} + func transformationModule(t *testing.T, source string) Module { t.Helper() ctx := NewContext() @@ -67,7 +130,8 @@ merge: t.Fatal("clone must own distinct debug information") } clone.Subprogram().SetSubprogramLinkageName("copy") - if !strings.Contains(mod.String(), `linkageName: "copy"`) || !strings.Contains(mod.String(), `linkageName: "original"`) { + ir := mod.String() + if !strings.Contains(ir, `linkageName: "copy"`) || !strings.Contains(ir, `linkageName: "original"`) { t.Fatal("debug linker names are not independent") } clone.Param(1).ReplaceAllUsesWith(ConstInt(mod.Context().Int1Type(), 1, false)) @@ -82,10 +146,11 @@ merge: break } } - if original.String() != before || strings.Contains(clone.String(), "fallback") || strings.Contains(clone.String(), "phi ") { - t.Fatalf("clone cleanup changed original or retained dead control flow:\n%s", clone.String()) + cloneIR := clone.String() + if original.String() != before || strings.Contains(cloneIR, "fallback") || strings.Contains(cloneIR, "phi ") { + t.Fatalf("clone cleanup changed original or retained dead control flow:\n%s", cloneIR) } - if !strings.Contains(clone.String(), "@external(") || clone.GetEnumFunctionAttribute(AttributeKindID("noinline")).IsNil() { + if !strings.Contains(cloneIR, "@external(") || clone.GetEnumFunctionAttribute(AttributeKindID("noinline")).IsNil() { t.Fatal("external references or attributes were lost") } decl := mod.NamedFunction("external").CloneFunction() @@ -115,18 +180,26 @@ define i32 @forward(i32 %x) { } `) call := mod.NamedFunction("forward").FirstBasicBlock().FirstInstruction() - for _, kind := range []TailCallKind{TailCallKindTail, TailCallKindNoTail, TailCallKindNone, TailCallKindMustTail} { - call.SetTailCallKind(kind) - if call.TailCallKind() != kind { - t.Fatalf("tail kind did not round trip: %d", kind) + for _, tc := range []struct { + kind TailCallKind + text string + }{ + {TailCallKindTail, "tail call"}, + {TailCallKindNoTail, "notail call"}, + {TailCallKindNone, "call"}, + {TailCallKindMustTail, "musttail call"}, + } { + call.SetTailCallKind(tc.kind) + if call.TailCallKind() != tc.kind { + t.Fatalf("tail kind did not round trip: %d", tc.kind) + } + if ir := call.String(); !strings.Contains(ir, " = "+tc.text+" ") { + t.Fatalf("tail kind %d has wrong IR semantics: %s", tc.kind, ir) } if err := VerifyModule(mod, ReturnStatusAction); err != nil { t.Fatal(err) } } - if !strings.Contains(call.String(), "musttail call") { - t.Fatal("musttail was downgraded to an optional hint") - } } func TestInlineAsmInfoAndMetadataConversion(t *testing.T) { @@ -134,10 +207,13 @@ func TestInlineAsmInfoAndMetadataConversion(t *testing.T) { defer ctx.Dispose() typ := FunctionType(ctx.VoidType(), []Type{ctx.Int32Type()}, false) for _, sideEffects := range []bool{false, true} { - asm := InlineAsm(typ, "# marker $0", "r", sideEffects, true, InlineAsmDialectIntel, true) + // Long strings exercise owned heap storage in older LLVM's std::string + // API; an embedded NUL also checks the length-carrying C boundary. + text := strings.Repeat("# marker $0\n", 64) + "\x00suffix" + asm := InlineAsm(typ, text, "r", sideEffects, true, InlineAsmDialectIntel, true) info := asm.InlineAsmInfo() rebuilt := InlineAsm(info.Type, info.Assembly, info.Constraints, info.HasSideEffects, info.IsAlignStack, info.Dialect, info.CanThrow) - if rebuilt != asm || info.HasSideEffects != sideEffects || !info.IsAlignStack || !info.CanThrow || info.Dialect != InlineAsmDialectIntel { + if rebuilt != asm || info.Assembly != text || info.Constraints != "r" || info.HasSideEffects != sideEffects || !info.IsAlignStack || !info.CanThrow || info.Dialect != InlineAsmDialectIntel { t.Fatalf("inline asm properties changed: %+v", info) } } @@ -146,4 +222,26 @@ func TestInlineAsmInfoAndMetadataConversion(t *testing.T) { t.Fatal("metadata conversion changed identity") } } + mod := ctx.NewModule("metadata-values") + defer mod.Dispose() + fn := AddFunction(mod, "add", FunctionType(ctx.Int32Type(), []Type{ctx.Int32Type()}, false)) + b := ctx.NewBuilder() + defer b.Dispose() + b.SetInsertPointAtEnd(ctx.AddBasicBlock(fn, "entry")) + constant := ConstInt(ctx.Int32Type(), 7, false) + sum := b.CreateAdd(fn.Param(0), constant, "sum") + b.CreateRet(sum) + for _, tc := range []struct { + value Value + kind MetadataKind + }{ + {constant, ConstantAsMetadataMetadataKind}, + {fn.Param(0), LocalAsMetadataMetadataKind}, + {sum, LocalAsMetadataMetadataKind}, + } { + md := tc.value.AsMetadata() + if md.Kind() != tc.kind || ctx.MetadataAsValue(md).AsMetadata() != md { + t.Fatalf("value metadata kind or identity changed: %s", tc.value.String()) + } + } }