Skip to content

Commit 397d41e

Browse files
mlechuc42f
andauthored
Docsystem: handle "semantically important docstring" cases (#80)
--------- Co-authored-by: Claire Foster <[email protected]>
1 parent 2630b65 commit 397d41e

File tree

4 files changed

+130
-7
lines changed

4 files changed

+130
-7
lines changed

src/compat.jl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,12 +275,14 @@ function _insert_convert_expr(@nospecialize(e), graph::SyntaxGraph, src::SourceA
275275
Expr(:MacroName, a12_esc(a12.value))))
276276
end
277277
elseif a1 isa GlobalRef && a1.mod === Core
278-
# TODO (maybe): syntax-introduced macrocalls are listed here for
279-
# reference. We probably don't need to convert these.
278+
# Syntax-introduced macrocalls are listed here for reference. We
279+
# probably don't need to convert these.
280280
if a1.name === Symbol("@cmd")
281-
elseif a1.name === Symbol("@doc")
281+
elseif a1.name === Symbol("@doc") && nargs === 4 # two macro args only
282+
# Single-arg @doc is a lookup not corresponding to K"doc"
283+
# Revise sometimes calls @doc with three args, but probably shouldn't
282284
st_k = K"doc"
283-
child_exprs = child_exprs[2:end]
285+
child_exprs = child_exprs[2:3]
284286
elseif a1.name === Symbol("@int128_str")
285287
elseif a1.name === Symbol("@int128_str")
286288
elseif a1.name === Symbol("@big_str")

src/desugaring.jl

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2857,7 +2857,7 @@ function is_invalid_func_name(ex)
28572857
return is_ccall_or_cglobal(name)
28582858
end
28592859

2860-
function expand_function_def(ctx, ex, docs, rewrite_call=identity, rewrite_body=identity)
2860+
function expand_function_def(ctx, ex, docs, rewrite_call=identity, rewrite_body=identity; doc_only=false)
28612861
@chk numchildren(ex) in (1,2)
28622862
name = ex[1]
28632863
if numchildren(ex) == 1 && is_identifier_like(name)
@@ -3023,6 +3023,35 @@ function expand_function_def(ctx, ex, docs, rewrite_call=identity, rewrite_body=
30233023
end
30243024
end
30253025

3026+
if doc_only
3027+
# The (doc str (call ...)) form requires method signature lowering, but
3028+
# does not execute or define any method, so we can't use function_type.
3029+
# This is a bit of a messy case in the docsystem which we'll hopefully
3030+
# be able to delete at some point.
3031+
sig_stmts = SyntaxList(ctx)
3032+
@assert first_default != 1 && length(arg_types) >= 1
3033+
last_required = first_default === 0 ? length(arg_types) : first_default - 1
3034+
for i in last_required:length(arg_types)
3035+
push!(sig_stmts, @ast(ctx, ex, [K"curly" "Tuple"::K"core" arg_types[2:i]...]))
3036+
end
3037+
sig_type = @ast ctx ex [K"where"
3038+
[K"curly" "Union"::K"core" sig_stmts...]
3039+
[K"_typevars" [K"block" typevar_names...] [K"block"]]
3040+
]
3041+
out = @ast ctx docs [K"block"
3042+
typevar_stmts...
3043+
[K"call"
3044+
bind_static_docs!::K"Value"
3045+
(kind(name) == K"." ? name[1] : ctx.mod::K"Value")
3046+
name_str::K"Symbol"
3047+
docs[1]
3048+
::K"SourceLocation"(ex)
3049+
sig_type
3050+
]
3051+
]
3052+
return expand_forms_2(ctx, out)
3053+
end
3054+
30263055
if !isnothing(return_type)
30273056
ret_var = ssavar(ctx, return_type, "return_type")
30283057
push!(body_stmts, @ast ctx return_type [K"=" ret_var return_type])
@@ -3083,10 +3112,11 @@ function expand_function_def(ctx, ex, docs, rewrite_call=identity, rewrite_body=
30833112
push!(method_stmts,
30843113
method_def_expr(ctx, ex, callex, method_table, main_typevar_names, arg_names,
30853114
arg_types, body, ret_var))
3115+
30863116
if !isnothing(docs)
30873117
method_stmts[end] = @ast ctx docs [K"block"
30883118
method_metadata := method_stmts[end]
3089-
@ast ctx docs [K"call"
3119+
[K"call"
30903120
bind_docs!::K"Value"
30913121
doc_obj
30923122
docs[1]
@@ -4273,6 +4303,27 @@ function expand_module(ctx, ex::SyntaxTree)
42734303
]
42744304
end
42754305

4306+
#-------------------------------------------------------------------------------
4307+
# Expand docstring-annotated expressions
4308+
4309+
function expand_doc(ctx, ex, docex, mod=ctx.mod)
4310+
if kind(ex) in (K"Identifier", K".")
4311+
expand_forms_2(ctx, @ast ctx docex [K"call"
4312+
bind_static_docs!::K"Value"
4313+
(kind(ex) === K"." ? ex[1] : ctx.mod::K"Value")
4314+
(kind(ex) === K"." ? ex[2] : ex).name_val::K"Symbol"
4315+
docex[1]
4316+
::K"SourceLocation"(ex)
4317+
Union{}::K"Value"
4318+
])
4319+
elseif is_eventually_call(ex)
4320+
expand_function_def(ctx, @ast(ctx, ex, [K"function" ex [K"block"]]),
4321+
docex; doc_only=true)
4322+
else
4323+
expand_forms_2(ctx, ex, docex)
4324+
end
4325+
end
4326+
42764327
#-------------------------------------------------------------------------------
42774328
# Desugaring's "big switch": expansion of some simple forms; dispatch to other
42784329
# expansion functions for the rest.
@@ -4339,7 +4390,7 @@ function expand_forms_2(ctx::DesugaringContext, ex::SyntaxTree, docs=nothing)
43394390
expand_forms_2(ctx, expand_compare_chain(ctx, ex))
43404391
elseif k == K"doc"
43414392
@chk numchildren(ex) == 2
4342-
sig = expand_forms_2(ctx, ex[2], ex)
4393+
expand_doc(ctx, ex[2], ex)
43434394
elseif k == K"for"
43444395
expand_forms_2(ctx, expand_for(ctx, ex))
43454396
elseif k == K"comprehension"

src/runtime.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,21 @@ function bind_docs!(type::Type, docstr, lineno::LineNumberNode; field_docs=Core.
315315
Docs.doc!(mod, bind, Base.Docs.docstr(docstr, metadata), Union{})
316316
end
317317

318+
"""
319+
Called in the unfortunate cases (K"call", K".", K"Identifier") where docstrings
320+
change the semantics of the expressions they annotate, no longer requiring the
321+
expression to execute.
322+
"""
323+
function bind_static_docs!(mod::Module, name::Symbol, docstr, lnn::LineNumberNode, sigtypes::Type)
324+
metadata = Dict{Symbol, Any}(
325+
:linenumber => lnn.line,
326+
:module => mod,
327+
:path => something(lnn.file, "none"),
328+
)
329+
bind = Base.Docs.Binding(mod, name)
330+
Docs.doc!(mod, bind, Base.Docs.docstr(docstr, metadata), sigtypes)
331+
end
332+
318333
#--------------------------------------------------
319334
# Runtime support infrastructure for `@generated`
320335

test/misc.jl

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,59 @@ end
104104
@test lower_str(Main, "x + y").args[1].has_image_globalref === true
105105
end
106106

107+
@testset "docstrings: doc-only expressions" begin
108+
local jeval(mod, str) = JuliaLowering.include_string(mod, str; expr_compat_mode=true)
109+
jeval(test_mod, "function fun_exists(x); x; end")
110+
jeval(test_mod, "module M end; module M2 end")
111+
# TODO: return values are to be determined, currently Base.Docs.Binding for
112+
# both lowering implementations. We can't return the value of the
113+
# expression in these special cases.
114+
jeval(test_mod, "\"docstr1\" sym_noexist")
115+
jeval(test_mod, "\"docstr2\" fun_noexist()")
116+
jeval(test_mod, "\"docstr3\" fun_exists(sym_noexist)")
117+
jeval(test_mod, "\"docstr4\" M.sym_noexist")
118+
jeval(test_mod, "\"docstr5\" M.fun_noexist()")
119+
jeval(test_mod, "\"docstr6\" M.fun_exists(sym_noexist)")
120+
@test jeval(test_mod, "@doc sym_noexist") |> string === "docstr1\n"
121+
@test jeval(test_mod, "@doc fun_noexist()") |> string === "docstr2\n"
122+
@test jeval(test_mod, "@doc fun_exists(sym_noexist)") |> string === "docstr3\n"
123+
@test jeval(test_mod, "@doc M.sym_noexist") |> string === "docstr4\n"
124+
@test jeval(test_mod, "@doc M.fun_noexist()") |> string === "docstr5\n"
125+
@test jeval(test_mod, "@doc M.fun_exists(sym_noexist)") |> string === "docstr6\n"
126+
@test jeval(test_mod.M, "@doc M.sym_noexist") |> string === "docstr4\n"
127+
@test jeval(test_mod.M, "@doc M.fun_noexist()") |> string === "docstr5\n"
128+
@test jeval(test_mod.M, "@doc M.fun_exists(sym_noexist)") |> string === "docstr6\n"
129+
130+
jeval(test_mod.M2, "\"docstr7\" M2.M2.sym_noexist")
131+
jeval(test_mod.M2, "\"docstr8\" M2.M2.fun_noexist()")
132+
jeval(test_mod.M2, "\"docstr9\" M2.M2.fun_exists(sym_noexist)")
133+
@test jeval(test_mod, "@doc M2.M2.sym_noexist") |> string === "docstr7\n"
134+
@test jeval(test_mod, "@doc M2.M2.fun_noexist()") |> string === "docstr8\n"
135+
@test jeval(test_mod, "@doc M2.M2.fun_exists(sym_noexist)") |> string === "docstr9\n"
136+
@test jeval(test_mod.M2, "@doc M2.M2.sym_noexist") |> string === "docstr7\n"
137+
@test jeval(test_mod.M2, "@doc M2.M2.fun_noexist()") |> string === "docstr8\n"
138+
@test jeval(test_mod.M2, "@doc M2.M2.fun_exists(sym_noexist)") |> string === "docstr9\n"
139+
140+
# Try with signatures and type variables
141+
jeval(test_mod, "abstract type T_exists end")
142+
143+
jeval(test_mod, "\"docstr10\" f10(x::Int, y, z::T_exists)")
144+
d = jeval(test_mod, "@doc f10")
145+
@test d |> string === "docstr10\n"
146+
# TODO: Is there a better way of accessing this? Feel free to change tests
147+
# if docsystem storage changes.
148+
@test d.meta[:results][1].data[:typesig] === Tuple{Int, Any, test_mod.T_exists}
149+
150+
jeval(test_mod, "\"docstr11\" f11(x::T_exists, y::U, z::T) where {T, U<:Number}")
151+
d = jeval(test_mod, "@doc f11")
152+
@test d |> string === "docstr11\n"
153+
@test d.meta[:results][1].data[:typesig] === Tuple{test_mod.T_exists, U, T} where {T, U<:Number}
154+
155+
jeval(test_mod, "\"docstr12\" f12(x::Int, y::U, z::T=1) where {T, U<:Number}")
156+
d = jeval(test_mod, "@doc f12")
157+
@test d |> string === "docstr12\n"
158+
@test d.meta[:results][1].data[:typesig] === Union{Tuple{Int64, U, T}, Tuple{Int64, U}} where {T, U<:Number}
159+
160+
end
161+
107162
end

0 commit comments

Comments
 (0)