Skip to content

Complete the debuginfo wrappers#549

Draft
maleadt wants to merge 27 commits into
mainfrom
tb/debuginfo
Draft

Complete the debuginfo wrappers#549
maleadt wants to merge 27 commits into
mainfrom
tb/debuginfo

Conversation

@maleadt
Copy link
Copy Markdown
Member

@maleadt maleadt commented Apr 24, 2026

Subsumes #152

maleadt and others added 26 commits April 24, 2026 13:14
Introduces the `DIBuilder` type wrapping `LLVMDIBuilderRef`, with
`allow_unresolved` kwarg, do-block form, `dispose`, `finalize!`, and
`finalize_subprogram!`. Follows the existing `IRBuilder` pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the first DIBuilder factory helpers, along with the DIModule
and DINamespace metadata types (both register as scopes). The
compileunit! signature mirrors PR #152's compilationunit! with the
producer/split-name/emission-kind/sysroot/sdk knobs exposed as kwargs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new DILocalScope subtypes and the matching lexicalblock!/
lexicalblockfile! builder methods. Tested alongside DILocation for
nested scopes and inlined_at chains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the full set of type constructors exposed by llvm-c/DebugInfo.h:
basic, derived (pointer/typedef/qualified/artificial/member/bitfield/
static/member-pointer/inheritance/reference), composite (struct/union/
class/array/vector/enumeration/forward-decl/replaceable), and
subroutine. Also adds getorcreatesubrange! and registers DIEnumerator
and DISubrange as metadata types. align(::DIType) and tag(::DINode)
accessors round out the read API.

LLVM-21-only additions (settype!, subrangetype!, dynamicarraytype!,
enumeratorarb!) are gated behind @static if version() >= v"21".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds DIBuilder methods for creating subprograms, local/parameter
variables, DIExpression and DIGlobalVariableExpression metadata, and
the global-variable forward declaration. Registers DIExpression and
DIGlobalVariableExpression as new metadata types and exposes the
variable/expression accessors on DIGlobalVariableExpression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds declare_before!, declare_at_end!, value_before!, value_at_end!
with LLVM-19-aware dispatch: intrinsic-based on LLVM ≤ 18 (returning
Instruction) and record-based on LLVM ≥ 19 (returning a new DbgRecord
wrapper). LLVM 20+ adds DILabel, label!, label_before!, label_at_end!.

debuglocation / debuglocation! on Instruction wraps
LLVMInstructionGet/SetDebugLoc and mirrors the existing IRBuilder
helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds wrappers for the four DIImportedEntity flavors, DIMacro /
DIMacroFile with macro!/tempmacrofile!, and registers the matching
metadata types. Label support was added in step 6 together with the
other LLVM-20-gated record helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds temporary_mdnode, dispose_temporary, and replace_all_uses_with!
for cycle-breaking in recursive debug-info types. replacearrays!
(rebinding a composite type's elements) and replacetype! (rewriting a
DISubProgram's type) are gated behind LLVM ≥ 21, where they were first
exposed through the C API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the five functions that were missed in the earlier passes:
getorcreatearray!, getorcreatetypearray!, objcivar!, objcproperty!,
and debug_metadata_version(::Module) for the per-module DWARF version.
Brings our coverage of llvm-c/DebugInfo.h to 100%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously users had to call LLVM.finalize! explicitly before dispose,
per the C API's stated contract. That's a footgun: forgetting it leaks
temporary metadata. dispose now calls LLVMDIBuilderFinalize first
(idempotent in LLVM), making the do-block pattern just-work. finalize!
stays callable for the rare case where the module has to be consumed
before dispose runs. finalize_subprogram! is no longer marked @public —
it's an advanced streaming helper, not a typical user API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DIBuilder::finalize() already iterates AllSubprograms and calls
finalizeSubprogram on each (DIBuilder.cpp:102). Unlike finalize!, the
subprogram-level call is not needed for correctness — only as an
optimization/streaming hint. Updated docstring to say so.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The common case (e.g. Julia code) has linkage_name empty or equal to
name, and scope_line equal to line. The C API requires both positionally;
Julia can do better by defaulting linkage_name="" (LLVM falls back to
name) and scope_line=line. Call sites previously reading
  subprogram!(dib, scope, "add", "add", file, 1, stype, 1)
now read
  subprogram!(dib, scope, "add", file, 1, stype).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds consttype!, volatiletype!, lvaluereferencetype!, and
rvaluereferencetype! as thin aliases for qualifiedtype! /
referencetype! with the relevant DW_TAG. Spares users from remembering
the four DWARF tag numbers that cover 95% of the qualified-type / C++
reference use cases.

DW_TAG constants themselves aren't re-exported — that's a separate,
principled job for all enums/constants in llvm-c. The wrappers hardcode
the numeric tags internally so they remain usable today.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches existing LLVM.jl precedent: multi-word descriptive names use
underscores (function_type, return_type, called_type, value_type,
fast_math, replace_metadata_uses!, parameter_attributes), while only
LLVM-jargon acronyms stay compressed (addrspace, callconv, datalayout).
The DIBuilder factories I originally landed as basictype!, pointertype!,
structtype!, compileunit!, autovariable!, etc. broke that rule; rename
them to basic_type!, pointer_type!, struct_type!, compile_unit!,
auto_variable!, etc.

Also folds the metadata-replace helper into the existing replace_uses!
(wrapping LLVMReplaceAllUsesWith on Value) as a Metadata method — same
semantics, cleaner dispatch than a parallel replace_all_uses_with!.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, `dispose` (and `finalize!`) unconditionally called
`LLVMDIBuilderFinalize`, which in assertion-enabled LLVM aborts with
"creating type nodes without a CU is not supported" when no compile unit
was registered.

Track CU registration in a `has_compile_unit::Ref{Bool}` field on
DIBuilder, set by `compile_unit!`. Finalization is skipped when the flag
is false; in release LLVM the C call also early-returns in that case, so
this only changes behavior under assertions.
`flags` clashes with the `flags=LLVMDIFlags` kwarg used on every type
factory on the same page; `cmdline` better conveys what the string is
(command-line options embedded verbatim in the emitted debug info).
LLVM 20 added a required `Implicit::LLVMBool` parameter to
`LLVMDIBuilderCreateObjectPointerType` that controls whether the
resulting type also carries `DI_FLAG_ARTIFICIAL`. Expose it as an
`implicit::Bool=true` kwarg on LLVM 20+ (preserving the LLVM ≤ 19
implicit-artificial behavior) and keep the two-argument call on older
LLVMs.
The C entry points (`LLVMDIBuilderInsertDeclare*` /
`LLVMDIBuilderInsertDbgValue*`) unconditionally `unwrap<DILocalVariable>`
the var argument; passing a `DIGlobalVariable` is undefined behavior
(asserts in debug LLVM, silent misbehavior in release). Reflect the
actual constraint in the Julia signatures; the docstrings already said
DILocalVariable.
Previously `subroutine_type!` took a single `Vector{<:Metadata}` whose
0th entry doubled as the return type -- a convention inherited straight
from the C API and easy to forget. Accept the return type as a separate
positional argument (with `nothing` for `void`) and let the parameter
vector mean what it says.
`LLVMDIBuilderCreateStaticMemberType` unconditionally `cast<Constant>`s
its `ConstantVal` operand and aborts on null (unlike the C++ entry
point, which calls `getConstantOrNull`). Make `constant_val` a required
positional argument so the foot-gun isn't reachable from the default-
argument path.
Passing `nothing` (the old default) was silently forwarded as C_NULL,
which LLVM segfaults on -- acknowledged by an `XXX:` comment in the
previous implementation. Make the scope required and throw an
`UndefRefError` from the explicit-`nothing` overload so the failure mode
is a Julia exception instead of a native crash.

The test that relied on the null-scope path is updated to attach a real
subprogram scope via a small DIBuilder.
Several testsets exercised patterns that LLVM silently tolerates in
release builds but rejects under assertions:

  - `lexical_block!` and `label!` were called with a `DICompileUnit`
    scope. LLVM's `getNonCompileUnitScope` drops CU scopes to null on
    these entry points, then asserts on the null. Nest both under a
    real `DISubprogram` scope instead.

  - `replaceable_composite_type!` was created and never RAUW'd in the
    type-constructors sweep; finalize then asserts on the unresolved
    temporary. It remains exercised in the mutation testset, where it
    is properly replaced.

Also add an `LLVM.verify(mod) === nothing` check after the end-to-end
build in the instruction-level-insertion testset -- a stronger
structural check than the existing `isa`/`occursin` assertions.
The underlying C entry points clone the input DIType with an extra
flag set, so the return kind is always whatever kind was passed in,
not specifically DIDerivedType. Wrap through Metadata(...)::DIType so
a DIBasicType stays a DIBasicType.
LLVMDIBuilderCreateObjCProperty returns a DIObjCProperty (metadata
kind 27), not a DIDerivedType. The old wrapper would trip typecheck
and, without it, emit an unidentified metadata kind on the first use.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

❌ Patch coverage is 85.18519% with 28 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/debuginfo.jl 85.18% 28 Missing ⚠️
@@            Coverage Diff             @@
##           master     #549      +/-   ##
==========================================
+ Coverage   85.91%   86.01%   +0.10%     
==========================================
  Files          47       47              
  Lines        2953     3140     +187     
==========================================
+ Hits         2537     2701     +164     
- Misses        416      439      +23     
Files with missing lines Coverage Δ
src/debuginfo.jl 86.61% <85.18%> (+3.27%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Docstrings placed inside @static if branches aren't picked up by
Documenter's checkdocs: the docstring ends up attached to a block
expression rather than the function binding, so `@docs LLVM.foo!`
reports "no docs found". Split every version-gated factory into an
outer, unconditional docstring on the bare name and version-selected
implementations below, and document the concrete DI*Type subtypes
(which have no standalone docstrings because they're generated in an
@eval loop) so cross-references to them resolve too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant