API — declaring
The mark itself, and reading marks back out. The rest of the surface is on API — observing and analysing and API — checking and releasing; the split is by source file, listed explicitly, so a new file with no page is a build failure rather than a silently missing section.
ExperimentalAPI.Mark — Type
MarkOne @experimental declaration.
reason is the field the whole design exists for: why a name is not settled is knowledge only the author has, so it travels with the mark rather than being reconstructed by a reader later. file/line point at the declaration, which is the definition site in the attached form.
| field | |
|---|---|
mod::Module | the module that wrote the mark, and where it is stored |
name::Symbol | the marked name; a macro is stored as Symbol("@foo") |
reason::String | why it is not settled |
since::Union{VersionNumber,Nothing} | version the name has been experimental since |
tracking::Union{String,Nothing} | where the shape is being decided — an issue, PR, or URL |
file::Symbol, line::Int | where the declaration is written |
sig::Union{Type,Nothing} | the signature it attached to, or nothing for a whole-name claim |
includes_constructors::Bool | a type's constructors are covered by its mark |
until::Union{Function,Nothing} | the exit condition — see ready_to_promote |
sig is what makes the claim narrower than the name. A mark attached to energy(::Numerical) says the name has something unsettled about it — audit reads it that way — while reach resolves calls to a method and only reports the marked one. A mark with sig === nothing (a name list, a struct, a const) covers every method the name has.
See experimental to read them back, and mark to look one up by name.
ExperimentalAPI.Mark — Method
Mark(mod, name, reason; since, tracking, file, line, sig, includes_constructors, until)Keyword form, for the fields that are usually absent. The positional form takes them in the order of the table in Mark.
ExperimentalAPI._newest_sig — Method
_newest_sig(ftype) -> Union{Type,Nothing}The signature of the most recently defined method under ftype.
Called from the statement the macro emits immediately after the definition, so "most recent" is the method that definition just created. Reading it back from the method table is the only route: the macro sees ::Numerical as syntax, and building Tuple{typeof(f), Numerical} out of syntax would mean evaluating names in a module that may not have them yet.
Returns nothing rather than throwing if the lookup fails — a mark that loses its signature is a wider claim, never a wrong one, and a package must not fail to load over it.
ExperimentalAPI.isnamewide — Method
isnamewide(mk::Mark) -> BoolWhether mk claims the whole name rather than one signature.
true for a name list, a struct, a const, a module — the forms that carry no signature. A mark attached to a definition knows which method it created and is therefore narrower; see stable for where the difference is load-bearing.
ExperimentalAPI.@experimental — Macro
@experimental "reason" definition
@experimental "reason" name₁ name₂ …
@experimental "reason" since=v"0.4.0" tracking="…" until=(() -> …) definitionDeclare that a public name is not settled, and say why.
The reason is required and comes first. Everything after it is either one definition or a list of names already defined elsewhere:
# attached to the definition
@experimental(
"signature will be wrapped once the write-back refactor settles",
function ingest(config; doc, kwargs...)
# ...
end,
)
# declared for names defined in an included file
@experimental(
"reads Test's internal result tree; not dogfooded in CI",
render_test_report,
dump_test_report,
load_test_dump,
)Optional keywords come between the reason and the subject:
| keyword | |
|---|---|
since=v"0.4.0" | the version the name has been unsettled since — read by age |
tracking="…" | the issue or PR where the shape is being decided |
until=() -> … | the exit condition: what would discharge the reason. See ready_to_promote |
tracking is what turns a mark into something a reader can act on. until is what turns it into a work item: a mark with no stated exit can be added but never mechanically retired, and marks_without_exit reports those.
Attaches to function, short-form f(x) = …, a callable object (c::C)(x) = …, a method on another module's generic Base.show(io, ::T) = …, struct, mutable struct, abstract type, primitive type, macro (recorded as Symbol("@name")), const, plain assignment, and a definition wrapped in @inline, @noinline, @generated, @propagate_inbounds, @assume_effects or Base.@kwdef.
Anything else — a module (which Julia requires as a direct top-level statement, so it cannot be wrapped), a definition produced by some other macro, a bare qualified name with no signature — is rejected with a message pointing at the name-list form, because guessing which name such an expression defines is exactly the kind of silence this package exists to remove.
Marking is not visibility: a marked name still has to be exported or declared public to be part of the surface. A mark on a name that is neither is reported by audit as dangling.
What the mark claims
A mark attached to a definition records the signature it created, so the claim is about that method. A mark written as a name list, or attached to a struct or a const, carries no signature and covers the whole name.
The difference shows up in three places: reach only reports the marked method, stable keeps a name in the covenant until every method behind it is marked, and audit reads either as "the author has said something about this name".
What gets observed
A definition with a body — function and short-form f(x) = …, including parametric, callable-object and return-type-annotated signatures, and those wrapped in an annotating macro such as @inline — also gets a flag in that body, so entered can report whether the run went through it. The flag is one short-circuit read: 1.03x on one thread and 0.985x on eight, measured over 10M calls of a numeric body.
Every other form is a declaration only — recorded, queryable and audited, but not observed:
| form | observed? |
|---|---|
function f(x) … end, f(x) = …, f(x::T) where {T} = …, f(x)::R = …, (c::C)(x) = … | yes |
Base.show(io::IO, ::T) = … — a method on a foreign generic | yes |
a name list (@experimental "…" a b c) | no — those definitions are elsewhere and already compiled |
struct, abstract type, primitive type, const, assignment | no — nothing is entered |
macro | no |
@inline, @noinline, @propagate_inbounds, @assume_effects, @constprop, @nospecializeinfer | yes — they annotate a definition and leave its body alone, so the flag rides inside |
@generated function | no — the body returns an expression, so a probe there would be generated rather than run |
Base.@kwdef | no — it wraps a struct, which has no body |
One flag per marked name, not per method: flags are name-keyed, so two methods of a marked name share one.
The mark is stored in a const binding in the enclosing module, so @experimental belongs at module top level — the same place export and public go. Written inside a function body it is refused by Julia's lowering, at the line you wrote it on.
See also experimental, audit, stable, reach.
ExperimentalAPI.experimental — Method
experimental(f) -> Vector{Mark}Every mark anyone has written on a method of the generic f, across all loaded modules.
The fetch_value(::Heisenberg, ::Energy) case: a generic is defined upstream and extended by several packages, each of which may declare its own methods unsettled. The marks live in the modules that wrote the methods, so answering this means asking all of them — see marks_on, which is the same search stated as a verb about the callee rather than about ownership.
ExperimentalAPI.experimental — Method
experimental(m::Module; extensions = false) -> Vector{Mark}Every declaration m has written with @experimental, sorted by name.
This is the query the marker exists to make possible — a tool asks the module, rather than a human reading docstrings. A module with no marks answers with an empty vector; it never errors for not having opted in.
for mk in experimental(MyPackage)
println(mk.name, " — ", mk.reason)
endIncludes marks on methods of other modules' generics, which m wrote and therefore owns: those carry a sig and a name that is not m's. extensions = true also walks m's package extensions, whose public names are part of the surface a user sees and are invisible to names(m).
The result is a fresh vector; mutating it does not change the module. Note that a mark does not imply the name is public — see audit's dangling.
ExperimentalAPI.experimental_methods — Method
experimental_methods(m::Module) -> Vector{Mark}The subset of m's marks that carry a signature — the claims about a method rather than about a whole name.
Every attached form produces one, including @experimental "…" Base.show(io, ::T) = …, which is how a package declares a method it contributed to somebody else's generic unsettled. The mark is stored in the module that wrote the method, never in the module that owns the name: a package cannot carry claims its dependents invented, and the mark has to survive that package being reloaded.
ExperimentalAPI.isexperimental — Method
isexperimental(m::Module, name::Symbol) -> Bool
isexperimental(m::Method) -> BoolWhether name in m, or the method m, is declared @experimental.
The one-bit form of mark, for a caller that only needs the verdict — for instance the mechanical statement that changing this name is not breaking. Does not consult docstrings and does not consult visibility: it answers only whether a mark exists.
ExperimentalAPI.mark — Method
mark(m::Method) -> Union{Mark,Nothing}The Mark covering the method m, or nothing.
A method is covered by a mark on its exact signature, or by a whole-name mark on the generic it belongs to. The second is why a name list still says something about every method behind the name, and the first is why marking one dispatch path does not speak for its siblings.
ExperimentalAPI.mark — Method
mark(m::Module, name::Symbol) -> Union{Mark,Nothing}The Mark on name in m, or nothing if it carries none.
This is how the reason gets to whoever needs it — an error message, a docs page, a release checklist:
mk = mark(MyPackage, :render_report)
mk === nothing || @warn "not settled" mk.reason mk.trackingA name may carry several marks when separate methods behind it were declared separately. The whole-name declaration is returned in preference to any of them, because it is the wider claim; marks returns all of them.
ExperimentalAPI.mark_method! — Method
mark_method!(m::Method, reason::AbstractString; since, tracking, until) -> MarkDeclare one method unsettled, from outside its definition.
The imperative route to what @experimental "…" f(::T) = … does at the definition site. It is here for the cases the macro cannot reach: a method produced by another package's macro, a method generated in a loop, and a package adopting this on code it does not want to touch yet.
The mark is stored in the module that wrote the method, m.module, exactly where the macro would have put it — so it is queryable through experimental_methods and survives the same way.
mark_method!(
which(fetch_value, Tuple{Heisenberg,Energy}),
"numerically delicate; no reference value";
tracking = "https://example.invalid/issues/12",
)Prefer the macro. A mark that is not next to the definition is a mark the next person editing that definition will not see.
ExperimentalAPI.marks — Method
marks(m::Module, name::Symbol) -> Vector{Mark}Every mark m carries under name — the whole-name declaration if there is one, and one per separately declared method.
ExperimentalAPI.marks_on — Method
marks_on(f) -> Vector{Mark}
marks_on(m::Method) -> Vector{Mark}Every mark written on a method of f, or on m specifically, wherever it was written.
The cross-module search. experimental_methods is the ownership question — "what has this module declared?" — and this is the call-site question — "has anyone said anything about what I am about to call?". One name for both would leave a reader unable to tell which they got.
ExperimentalAPI.marks_without_exit — Method
marks_without_exit(m::Module) -> Vector{Mark}The marks that say why a name is unsettled but not what would settle it.
A mark with no until= can be added and never mechanically retired — ready_to_promote can only answer false for it, forever. That is a decoration rather than a work item, so it is reported as its own finding rather than being folded into "not ready".
ExperimentalAPI.package_extensions — Method
package_extensions(m::Module) -> Vector{Module}Every loaded package extension of m.
An extension is a separate module whose public names are part of the surface a user sees and are invisible to names(m). audit reports which of them are loaded rather than folding them in, because an extension that is not loaded is not missing — it is inapplicable.
ExperimentalAPI.superseded_marks — Method
superseded_marks(m::Module) -> Vector{Mark}The declarations that were replaced by a later one on the same name and signature.
Re-marking is last-write-wins, which is the right rule — a file included twice must not double its marks — but it is a rule that can lose a reason silently. Anything it drops is kept here, so "the mark says something different from what I wrote" is answerable rather than mysterious.
Empty for almost every module: nothing is recorded unless a mark actually replaced a different one.
Declared unfinished
Generated from the package's own marks at build time by this package's own Documenter extension, so it cannot go stale — and it is the same call any consumer would make:
@entered — the report is a text format with no schema, and it has already changed twice in its first week — the hit lines gained a sort order and column alignment, the footer a singular verb. Anything that parses this output is parsing a guess
*covers every method of the name · **no exit condition recorded***
Attribution — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
Diff — the snapshot schema is young: nothing has been released against it, and the method half was added after the name half, so a file written by one version may not be readable by the next
*covers every method of the name · **no exit condition recorded***
Hit — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
MethodDiff — the snapshot schema is young: nothing has been released against it, and the method half was added after the name half, so a file written by one version may not be readable by the next
*covers every method of the name · **no exit condition recorded***
Reach — the line between :clean and :unknown is drawn by Julia's own IR accessors, which are internal and differ by minor version: a statement's line comes from Base.IRShow.getdebugidx on 1.12 and from codelocs/linetable on 1.11, and a callee spells itself Core.TypeEgal{T} on 1.14-DEV where earlier versions say Type{T}. A verdict that moves with the compiler is not yet a covenant
*covers every method of the name · **no exit condition recorded***
Reached — the line between :clean and :unknown is drawn by Julia's own IR accessors, which are internal and differ by minor version: a statement's line comes from Base.IRShow.getdebugidx on 1.12 and from codelocs/linetable on 1.11, and a callee spells itself Core.TypeEgal{T} on 1.14-DEV where earlier versions say Type{T}. A verdict that moves with the compiler is not yet a covenant
*covers every method of the name · **no exit condition recorded***
Record — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
TimingBackend — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
Unresolved — the line between :clean and :unknown is drawn by Julia's own IR accessors, which are internal and differ by minor version: a statement's line comes from Base.IRShow.getdebugidx on 1.12 and from codelocs/linetable on 1.11, and a callee spells itself Core.TypeEgal{T} on 1.14-DEV where earlier versions say Type{T}. A verdict that moves with the compiler is not yet a covenant
*covers every method of the name · **no exit condition recorded***
Verification — the counts come from two interfaces Julia does not document — ccall(:jl_write_coverage_data, …) to flush from the running process, and the .cov line format to read them back — so a Julia release can change what this reports without anything here changing
*covers every method of the name · **no exit condition recorded***
assert_clean — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
attribute — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
combine — the line between :clean and :unknown is drawn by Julia's own IR accessors, which are internal and differ by minor version: a statement's line comes from Base.IRShow.getdebugidx on 1.12 and from codelocs/linetable on 1.11, and a callee spells itself Core.TypeEgal{T} on 1.14-DEV where earlier versions say Type{T}. A verdict that moves with the compiler is not yet a covenant
*covers every method of the name · **no exit condition recorded***
compare — the snapshot schema is young: nothing has been released against it, and the method half was added after the name half, so a file written by one version may not be readable by the next
*covers every method of the name · **no exit condition recorded***
compare_methods — the snapshot schema is young: nothing has been released against it, and the method half was added after the name half, so a file written by one version may not be readable by the next
*covers every method of the name · **no exit condition recorded***
coverage — the counts come from two interfaces Julia does not document — ccall(:jl_write_coverage_data, …) to flush from the running process, and the .cov line format to read them back — so a Julia release can change what this reports without anything here changing
*covers every method of the name · **no exit condition recorded***
coverage_enabled — the counts come from two interfaces Julia does not document — ccall(:jl_write_coverage_data, …) to flush from the running process, and the .cov line format to read them back — so a Julia release can change what this reports without anything here changing
*covers every method of the name · **no exit condition recorded***
dependents — the line between :clean and :unknown is drawn by Julia's own IR accessors, which are internal and differ by minor version: a statement's line comes from Base.IRShow.getdebugidx on 1.12 and from codelocs/linetable on 1.11, and a callee spells itself Core.TypeEgal{T} on 1.14-DEV where earlier versions say Type{T}. A verdict that moves with the compiler is not yet a covenant
*covers every method of the name · **no exit condition recorded***
experimental_fraction — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
flush_coverage — the counts come from two interfaces Julia does not document — ccall(:jl_write_coverage_data, …) to flush from the running process, and the .cov line format to read them back — so a Julia release can change what this reports without anything here changing
*covers every method of the name · **no exit condition recorded***
isbreaking — the snapshot schema is young: nothing has been released against it, and the method half was added after the name half, so a file written by one version may not be readable by the next
*covers every method of the name · **no exit condition recorded***
isclean — the line between :clean and :unknown is drawn by Julia's own IR accessors, which are internal and differ by minor version: a statement's line comes from Base.IRShow.getdebugidx on 1.12 and from codelocs/linetable on 1.11, and a callee spells itself Core.TypeEgal{T} on 1.14-DEV where earlier versions say Type{T}. A verdict that moves with the compiler is not yet a covenant
*covers every method of the name · **no exit condition recorded***
merge_records — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
reach — the line between :clean and :unknown is drawn by Julia's own IR accessors, which are internal and differ by minor version: a statement's line comes from Base.IRShow.getdebugidx on 1.12 and from codelocs/linetable on 1.11, and a callee spells itself Core.TypeEgal{T} on 1.14-DEV where earlier versions say Type{T}. A verdict that moves with the compiler is not yet a covenant
*covers every method of the name · **no exit condition recorded***
reach_script — the line between :clean and :unknown is drawn by Julia's own IR accessors, which are internal and differ by minor version: a statement's line comes from Base.IRShow.getdebugidx on 1.12 and from codelocs/linetable on 1.11, and a callee spells itself Core.TypeEgal{T} on 1.14-DEV where earlier versions say Type{T}. A verdict that moves with the compiler is not yet a covenant
*covers every method of the name · **no exit condition recorded***
read_record — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
read_snapshot — the snapshot schema is young: nothing has been released against it, and the method half was added after the name half, so a file written by one version may not be readable by the next
*covers every method of the name · **no exit condition recorded***
record — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
recording — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
snapshot — the snapshot schema is young: nothing has been released against it, and the method half was added after the name half, so a file written by one version may not be readable by the next
*covers every method of the name · **no exit condition recorded***
stale_marks — the counts come from two interfaces Julia does not document — ccall(:jl_write_coverage_data, …) to flush from the running process, and the .cov line format to read them back — so a Julia release can change what this reports without anything here changing
*covers every method of the name · **no exit condition recorded***
stamp — the snapshot schema is young: nothing has been released against it, and the method half was added after the name half, so a file written by one version may not be readable by the next
*covers every method of the name · **no exit condition recorded***
timing_backend — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
unverified — the counts come from two interfaces Julia does not document — ccall(:jl_write_coverage_data, …) to flush from the running process, and the .cov line format to read them back — so a Julia release can change what this reports without anything here changing
*covers every method of the name · **no exit condition recorded***
verdict — the line between :clean and :unknown is drawn by Julia's own IR accessors, which are internal and differ by minor version: a statement's line comes from Base.IRShow.getdebugidx on 1.12 and from codelocs/linetable on 1.11, and a callee spells itself Core.TypeEgal{T} on 1.14-DEV where earlier versions say Type{T}. A verdict that moves with the compiler is not yet a covenant
*covers every method of the name · **no exit condition recorded***
verification — the counts come from two interfaces Julia does not document — ccall(:jl_write_coverage_data, …) to flush from the running process, and the .cov line format to read them back — so a Julia release can change what this reports without anything here changing
*covers every method of the name · **no exit condition recorded***
write_record — the record's shape and its collection knobs are both still moving: Record gained a field after write_record already had a file format, so a file written by an earlier version reads back with that field empty, and paths and timing are refused together because the pair segfaulted 2 runs in 4 where each alone crashed 0 in 4
*covers every method of the name · **no exit condition recorded***
write_snapshot — the snapshot schema is young: nothing has been released against it, and the method half was added after the name half, so a file written by one version may not be readable by the next
*covers every method of the name · **no exit condition recorded***
A docstring says what a name does. That block says whether it is finished. Nothing on this page is undocumented — the names above are documented and declared, which is the normal state for something that works but whose shape is still being argued about.