Skip to content

Julia 1.13: re-wrap @doc cell result as Docs.Binding - #3546

Draft
pankgeorg wants to merge 3 commits into
mainfrom
pg/julia-1.13-docstring-display
Draft

Julia 1.13: re-wrap @doc cell result as Docs.Binding#3546
pankgeorg wants to merge 3 commits into
mainfrom
pg/julia-1.13-docstring-display

Conversation

@pankgeorg

@pankgeorg pankgeorg commented May 14, 2026

Copy link
Copy Markdown
Member

Summary

Restore the docstring-as-cell-output rendering on Julia 1.13+ that was broken by JuliaLang/julia#59882. Fixes the failing assertions in #3449; part of the #3389 umbrella. Continues — but does not stack on — the closed #3452, which had been abandoned because the author could not pin down the upstream behavior change.

What changed in Julia 1.13

Same parsed expression, different result:

julia> Meta.parse("\"::Bool\"\nf(::Bool) = 1") |> e -> Core.eval(Main, e) |> typeof
# 1.12: Base.Docs.Binding
# 1.13: typeof(f)

@doc str def now returns the value of def instead of a Base.Docs.Binding (JuliaLang/julia#60681, closed as not planned). Pluto's format_output(::Base.Docs.Binding) dispatch no longer fires, so documented function cells render as f (generic function with N methods) rather than the docstring HTML.

Docs.doc(binding) itself still works correctly on 1.13 and aggregates all method docstrings as before — only the cell result type changed.

The fix

Detect the (pre-macroexpansion) @doc macrocall in run_expression, extract the bound name from the definition, and re-wrap the result as Docs.Binding(workspace, name) before storing in cell_results. This puts the cell back on the existing rendering path; no display-side changes required.

  • No-op on Julia < 1.13 (where the result is already a Docs.Binding).
  • Skipped for CapturedException results so error rendering is untouched.
  • Skipped if the bound name isn't defined in the workspace, so a half-failed definition still falls through to the default formatter.
  • Handles common doc targets: f(x) = …, function f end, f(::T) where T, parametric methods, struct, macro, module, const.

Test plan

  • CI: test/MacroAnalysis.jl "Doc strings" testset (the 11 failing assertions from Julia 1.13 tests: Macro analysis - docstrings #3449) passes on Julia 1.13.
  • CI: same testset still passes on 1.11 and 1.12 (helper is a no-op).
  • Manual: open a notebook on 1.13, run a cell like "docstring"\nf(::Bool) = 1, verify the rich docstring panel renders.
  • Manual: redefine a method in another cell, verify both method docstrings appear in either cell's output.

Out of scope (follow-ups from #3389)

  • warn_julia_compat() in src/Pluto.jl still warns on 1.13 — should be bumped to 1.14 once the testset is green.
  • Malt.jl#101 and #93 (1.13 interrupt failures) are still open.
  • GracefulPkg.jl 1.13 audit per the umbrella issue.

Try this Pull Request!

Open Julia and type:

julia> import Pkg
julia> Pkg.activate(temp=true)
julia> Pkg.add(url="https://github.com/JuliaPluto/Pluto.jl", rev="pg/julia-1.13-docstring-display")
julia> using Pluto

@pankgeorg

pankgeorg commented May 25, 2026

Copy link
Copy Markdown
Member Author

Another idea:

  • Make a pluto-specific show method for Functions that always gets the latest docs (after reactivity is done rendering)
    • in order to get the function to display the "same" docstring across methods, and not different, based on the timing of its execution
  • Make that work in the pre - 1.12
  • Keep the docs renderer in case someone actually explicitly requests it.

@fonsp

fonsp commented May 25, 2026

Copy link
Copy Markdown
Member

Little nsippet

<div style="
font-family: JuliaMono;
padding: .4em .4em;
font-size: .8rem;
">

<span style='font-style: italic; opacity: .8;'>function</span>

<span style='font-weight: bold;'>hello_world</span>

<a style='
	font-family: system-ui;
margin-inline-start: 2ch;
' href="#">(View docs)</a>

</div>

@j-fu

j-fu commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Hi, with 1.13 release approaching I tested this PR (with current main merged in) - CI tests including those from MacroAnalysis are all green for both 1.12.6 and 1.13-rc1 (openSUSE Leap 15.6 Linux).
Also no issues with Malt.

So this would tick all the items in the test plan green.

I am living on 1.13 for quite while now including a multitude of Pluto notebook uses, and I have seen no other hiccups.

So IMHO the "unsupported" warning can be bumped and this can be merged.

@j-fu

j-fu commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

This is what Claude Sonnet has to say:

I compared jf/julia-1.13-docstring-display against main (only one file changed: src/runner/PlutoRunner/src/evaluation/run_expression.jl) and empirically tested the fix against a real Julia 1.13.0-rc1 install plus 1.12.6 for comparison. Summary of the change first, then risks.

What the change does

Julia 1.13 changed @doc str def to return the value of def instead of a Docs.Binding (JuliaLang/julia#59882), which broke Pluto's special format_output(::Docs.Binding) rendering (the nice docs panel). The fix adds doc_macrocall_binding_name/doc_target_name to re-derive the bound name from the pre-macroexpansion cell expression, and maybe_rewrap_as_doc_binding re-wraps the result as a Docs.Binding on Julia ≥ 1.13 only.

Validated as working

  • Ran the existing test/MacroAnalysis.jl "Doc strings" testset against Julia 1.13.0-rc1 with the branch code (via TestEnv.activate() in the repo's own project) — all 22 assertions pass, including the trickier Base.conj() doc-merging case.
  • The @static if VERSION < v"1.13.0-DEV" guard is correct: since prerelease versions sort below their release, 1.13.0, 1.13.0-rc*, and 1.13.0-DEV.xxxx nightlies all take the new path, while all 1.12.x take the no-op path.
  • @doc x / @doc sin (lookup form) are correctly left alone (guarded by length(expr.args) >= 4), and a begin...end block with a stray string + multiple statements is correctly not misdetected as a doc cell.
  • cell_results is only read by format_output (display), not by Pluto's reactivity/dependency graph, so this only affects rendering, not notebook semantics — no risk to reactivity.
  • No new attack surface: this only introspects the user's own already-trusted AST; no security concern.

Risk: the heuristic silently misses several common definition shapes

I confirmed three concrete cases where doc_target_name fails to find a name, so maybe_rewrap_as_doc_binding falls through to the pre-1.13 (broken/plain) rendering — no error is raised, it just silently degrades, which is what makes this risky (it won't show up as a test failure or exception, only as "the docs panel doesn't appear"):

  1. Macros: "doc" \n macro foo(x) ... end extracts the bare name :foo, but the macro is bound as var"@foo", not foo. isdefined(m, :foo) is false, so it's never rewrapped. Verified: cell shows @mymac (macro with 1 method) instead of the docs panel, whereas Julia 1.12 returns a Docs.Binding directly for the exact same code (so this is a real regression vs. pre-1.13 Pluto for macros).
  2. Structs with an explicit supertype (struct MyStruct2 <: Number ... end): the AST for the type signature has head :<:, which isn't among the handled heads in doc_target_name, so it returns nothing. Verified: a plain struct MyStruct3 gets the nice pluto-docs-binding HTML, but struct MyStruct2 <: Number renders as a bare text/plain cell with an empty body.
  3. abstract type/primitive type declarations: their AST heads are :abstract/:primitive, neither handled at all, so any docstring on a type hierarchy declaration (a very common Julia pattern) is silently dropped.

These aren't edge cases nobody writes — subtyped structs, abstract type hierarchies, and documented macros are all idiomatic Julia. They currently work fine on 1.12 (via the old direct-Docs.Binding return), so this is a partial, not full, fix of the regression, and it fails invisibly.

Other observations

  • No new tests were added for doc_macrocall_binding_name/doc_target_name/maybe_rewrap_as_doc_binding themselves; the PR relies entirely on the pre-existing "Doc strings" testset, which happens to only exercise the shapes that already work (plain function/struct/const definitions, dotted Base extension). None of the three gaps above would be caught by CI.
  • The approach is inherently coupled to the exact AST shape Julia's parser produces for @doc and to the assumption that "Pluto's Parse.jl always wraps cells in :toplevel/:block". That's fine today (validated), but it's a second, separate implicit contract with Julia's parser that could silently drift in a future Julia release without any test failure (the function fails open, returning the un-wrapped value, rather than erroring).

Recommendation

I would not block merging over this — it's a strict improvement over the current broken 1.13 rendering for the common cases, is well-guarded by version checks, has no security/reactivity implications, and passes existing tests. But I'd flag it for follow-up before/soon after merging:

  • Extend doc_target_name to handle :abstract, :primitive, and :<: (treat like :where/:curly — recurse into args[1]).
  • Fix macro name resolution to check for the var"@name" binding (or Symbol("@", name)) in addition to the plain name.
  • Add regression tests for these three shapes (and maybe mutable struct <: X, abstract type <: X) so future changes don't silently regress them again.

I asked them to implement these recommendations.

@j-fu

j-fu commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

See #3600

pankgeorg and others added 3 commits July 27, 2026 13:44
JuliaLang/julia#59882 changed `@doc` to return the documented value (e.g. the
function itself) instead of a `Docs.Binding`. Pluto's docstring cell output
relies on dispatching on `Base.Docs.Binding` via `format_output`, so on 1.13
documented function cells now render as "f (generic function with N methods)"
instead of the rich docstring view. Causes the test failures in #3449.

Detect the pre-macroexpansion `@doc` macrocall, extract the bound name, and
re-wrap the cell result as `Docs.Binding(workspace, name)` so the existing
display path is preserved. No-op on Julia < 1.13.

Refs JuliaLang/julia#60681 (closed as not planned), #3449, #3389.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pluto's Parse.jl always returns Expr(:toplevel, LineNumberNode, inner), so the
original_expr seen by run_expression is wrapped — the earlier helper only
inspected a bare :macrocall and returned nothing for the real shape, which is
why the test/MacroAnalysis.jl "Doc strings" assertions still failed on 1.13 CI.
Walk past :toplevel/:block wrappers (ignoring LineNumberNodes) before checking
for the @doc macrocall.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
doc_target_name returned nothing for `Expr(:., :Base, QuoteNode(:conj))`, so
the cell "An empty conjugate"\nBase.conj() = x left its result as `nothing`
on 1.13 and the last two MacroAnalysis.jl "Doc strings" assertions kept
failing. Pick the rightmost symbol from a qualified name; Docs.Binding in any
workspace resolves to the same docs via Docs.aliasof, so a workspace-rooted
binding still pulls in Base.conj's docstring plus the new one we added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pankgeorg
pankgeorg force-pushed the pg/julia-1.13-docstring-display branch from 704beb1 to 75b5873 Compare July 27, 2026 10:44
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.

3 participants