Skip to content

fix: an @inline marked kernel registered a probe nothing could ever set - #28

Merged
sotashimozono merged 1 commit into
mainfrom
fix/the-inline-probe-was-never-inserted
Sep 8, 2026
Merged

fix: an @inline marked kernel registered a probe nothing could ever set#28
sotashimozono merged 1 commit into
mainfrom
fix/the-inline-probe-was-never-inserted

Conversation

@sotashimozono

Copy link
Copy Markdown
Member

The question was whether @experimental itself has settled. The churn says yes — src/mark.jl has
not been touched since #13, across the fourteen commits since, while everything around it moved. So
I put the question to a sweep of 25 definition forms instead of to the git log, and the sweep found
one hole, in the class this package exists to close.

The finding

@experimental "kernel unverified" @inline f(x) = x     # mark outside
@inline @experimental "kernel unverified" g(x) = x     # mark inside

f(1); g(1)
entered(M)   # → [:g]

Both are marked. Both register a probe, so both count toward "observable marked definitions".
Only one can ever be entered. With the mark outside:

what asked what it said
entered not entered, no matter what ran
@entered / record a clean run through unvalidated code
unverified reported forever

Six macros affected: @inline, @noinline, @propagate_inbounds, @assume_effects,
@constprop, @nospecializeinfer. An @inline kernel is exactly the small numeric body this
package is aimed at.

Cause: two halves of one feature, written to different assumptions

_subject (src/mark.jl) was right — those six are in _ANNOTATING_MACROS, the wrapper is rebuilt
around the definition, instrumentable is passed through, and the comment says "an @inline
marked kernel is exactly the kind that has to be observable"
.

_instrument (src/detect.jl) began (def.head === :function || def.head === :(=)) || return nothing and handed back nothing for the :macrocall it was given. The probe was dropped; the
flag was registered anyway.

It now recurses through the wrapper and rebuilds it around the instrumented body.

Two documents that described the defect rather than the design

The docstring table read `Base.@kwdef`, `@inline`, `@noinline` and the other pass-through macros | no — which is what the code did, not what the code was for. Split into two rows, each
saying why: the annotating six are observed; @generated (its body returns an expression) and
Base.@kwdef (it wraps a struct) are not.

The test was named "@inline and the mark compose in both orders" and asserted
Set([:f, :g]) == names marked — satisfied by a mark that can never fire. The two orders did not
compose the same way and the assertion was structurally unable to see it. It now asserts the
observation, and a second testset pins the split a careless fix would break: instrumenting every
macrocall would put a probe inside @generated's returned expression.

Three refusals that no test touched

Measured by grepping test/ for their message text — zero hits each:

refused now pinned on
a wrapping macro this cannot read the name of the macro, and @experimental "why" the_name
begin f(x)=x; g(x)=x end the word block and the alternative — half-marking two definitions is the silence this package removes
a bare Sub.g Sub.g, WHICH method, and Sub.g(:: — the form that works, spelled with their own name

Incidental: a macro that throws while expanding a module body run through include_string comes
back wrapped in LoadError twice, so the usual single .error leaves a LoadError that reads
exactly like the failure it hides.

186 behaviours, 1210 assertions, green.

🤖 Generated with Claude Code

Asked whether `@experimental` itself has settled, the churn says yes — `src/mark.jl` has not been
touched since #13, across the fourteen commits since. So the question was put to a sweep of 25
definition forms instead, and the sweep found one hole, in the class this package exists to close.

    @experimental "kernel unverified" @inline f(x) = x     # mark outside
    @inline @experimental "kernel unverified" g(x) = x     # mark inside

    entered(M)  →  [:g]

Both are marked. Both **register a probe**, so both count as observable marked definitions. Only
one can ever be entered. With the mark outside, `entered` said "not entered" no matter what ran,
`@entered` and `record` reported a clean run through unvalidated code, and `unverified` reported
the definition forever.

`_subject` was right: `@inline` and its five neighbours are in `_ANNOTATING_MACROS`, the wrapper is
rebuilt around the definition, and `instrumentable` is passed through — with a comment saying "an
`@inline` marked kernel is exactly the kind that has to be observable". `_instrument`, in the other
file, began `(def.head === :function || def.head === :(=)) || return nothing` and handed back
`nothing` for the `:macrocall` it was given. Two halves of one feature written to different
assumptions. It now recurses through the wrapper and rebuilds it around the instrumented body.

**The docstring documented the defect.** Its table read "`Base.@kwdef`, `@inline`, `@noinline` and
the other pass-through macros | no", which is what the code did, not what the code was for. The row
is now split: the six annotating macros are observed; `@generated` (its body returns an expression)
and `Base.@kwdef` (it wraps a struct) are not, and each says why.

**The test was named for the claim and asserted something weaker.** `@testset "@inline and the mark
compose in both orders"` asserted `Set([:f, :g]) == names marked` — satisfied by a mark that can
never fire. It now asserts the observation, and a second testset pins the split that a careless fix
would break: instrumenting every macrocall would put a probe inside `@generated`'s returned
expression.

Three refusals had no test at all — measured by grepping `test/` for their message text and finding
zero hits: a wrapping macro this cannot read, a `begin` block with two definitions in it, and a bare
qualified name. Each is now pinned on the part that makes it actionable — the name of the macro it
could not read, the word `block`, `Sub.g` and the form that would work — rather than on the fact
that something was thrown.

One incidental find while pinning them: a macro that throws while expanding a `module` body run
through `include_string` comes back wrapped in `LoadError` **twice**, so the usual single `.error`
leaves a `LoadError` that reads exactly like the failure it hides.

186 behaviours, 1210 assertions, green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the bug Something isn't working label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📚 Docs preview: https://codes.sota-shimozono.com/ExperimentalAPI.jl/previews/PR28/

(updates on each push to this PR)

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@sotashimozono
sotashimozono merged commit af2999a into main Sep 8, 2026
14 checks passed
@sotashimozono
sotashimozono deleted the fix/the-inline-probe-was-never-inserted branch September 8, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant