Compare commits

...

24 Commits

Author SHA1 Message Date
TEC 8d695e5e28
Initial support for about(::Module)
Co-authored-by: Sasha Demin <demin@lix.polytechnique.fr>
2024-05-02 03:12:49 +08:00
TEC 4967bcd976
Add a little more structure to the codebase 2024-05-01 23:18:51 +08:00
TEC d4e923c23c
Add S"" as shorthand for styled""
We're about to reach 100 instances of styled"", and at this point I'm
rather keen to avoid typing it out in full each time and save a good few
hundred characters.
2024-05-01 23:05:16 +08:00
TEC 172a33cb1f
Show return types of functions 2024-05-01 22:35:09 +08:00
TEC 9ab30d97f0
More thorough effect printing 2024-05-01 22:02:10 +08:00
TEC 04869e3b4c
Show function effects via specialised about call 2024-04-29 20:28:08 +08:00
TEC 1b563854d9
Support more compiler effects 2024-04-29 01:36:05 +08:00
TEC 35aeef29b0
Avoid showing sign twice with int, as we handle it 2024-04-12 16:45:31 +08:00
TEC 1305239f01
Fix printing of GenericMemory
This makes it no longer error, a cleverer display can be worked out in
the future.
2024-04-12 16:36:25 +08:00
TEC 7dbce9529e
Prevent double ups of black lines in printing 2024-04-12 16:35:54 +08:00
TEC 4e23083d0f
Much fancier char unicode layout printing 2024-04-10 22:54:38 +08:00
TEC 70871a6b60
Elaborate on character usage/meaning 2024-04-10 22:54:38 +08:00
TEC b0c7025372
Improved singleton value handling in layout 2024-04-10 22:54:38 +08:00
TEC e0025cd0a2
Improved value size printing 2024-04-10 22:54:38 +08:00
TEC fa0a49b606
Support elaboration on values 2024-04-10 18:18:28 +08:00
TEC 201d9db07d
Slightly improve generic memory layout printing 2024-04-10 18:05:49 +08:00
TEC 4cef33ae79
Show richer type parameter information 2024-04-08 23:29:29 +08:00
TEC 40f29a9bd0
Implement specialised composed function printing 2024-04-08 23:29:29 +08:00
TEC 19fe198ba4
Define some custom faces
Hardcoding colours is so `printstyled` 😛
2024-04-08 23:29:29 +08:00
TEC d67e9a3bce
Use JuliaSyntaxHighlighting for nicer printing 2024-04-08 23:29:29 +08:00
TEC 7dd9352d70
Fix handling of empty struct fields
Structs can have singleton fields that take up no space.
2024-04-08 23:29:29 +08:00
TEC ae3e6b7403
Recognise singleton values 2024-04-08 23:29:29 +08:00
Sasha Demin df6f20fbf6
Relax sizeof conditions in structinfo 2024-03-21 17:00:28 +08:00
Sasha Demin 6ac75ded73
Fix for non-concrete typess 2024-03-21 17:00:28 +08:00
7 changed files with 681 additions and 112 deletions

View File

@ -5,8 +5,17 @@ version = "0.1.0"
[deps]
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
JuliaSyntaxHighlighting = "dc6e5ff7-fb65-4e79-a425-ec3bc9c03011"
StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b"
[weakdeps]
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
[extensions]
PkgExt = "Pkg"
[compat]
InteractiveUtils = "1.11.0"
JuliaSyntaxHighlighting = "1.11.0"
Pkg = "1.11.0"
StyledStrings = "1.11.0"

78
ext/PkgExt.jl Normal file
View File

@ -0,0 +1,78 @@
module PkgExt
using Pkg
using StyledStrings
import About: about_pkg, columnlist
function about_pkg(io::IO, pkg::Base.PkgId, mod::Module)
isnothing(pkgversion(mod)) ||
print(io, styled" Version {about_module:$(pkgversion(mod))}")
srcdir = pkgdir(mod)
if isnothing(srcdir)
print(io, styled" (builtin)")
else
srcdir = Base.fixup_stdlib_path(srcdir)
srcdir = something(Base.find_source_file(srcdir), srcdir)
srcdir = contractuser(srcdir)
print(io, styled" loaded from {light,underline:$srcdir}")
end
println(io)
isnothing(srcdir) && return
manifest_file = Pkg.Types.manifestfile_path(pkgdir(mod))
thedeps = if !isnothing(manifest_file) && isfile(manifest_file)
Pkg.Types.read_manifest(manifest_file).deps
else
Pkg.dependencies()
end
directdeps = if haskey(thedeps, pkg.uuid)
listdeps(thedeps, pkg.uuid)
else
collect(keys(thedeps))
end
isempty(directdeps) && return
depstrs = map(directdeps) do dep
nindirect = length(alldeps(thedeps, dep))
if nindirect > 0
styled"$(thedeps[dep].name) {shadow:(+$nindirect)}"
else
styled"$(thedeps[dep].name)"
end
end
indirect_depcount = length(alldeps(thedeps, pkg.uuid) directdeps) - length(depstrs)
indirect_info = if indirect_depcount > 0
styled" {shadow:(+$indirect_depcount indirectly)}"
else styled"" end
println(io, styled"\n{bold:Directly depends on {emphasis:$(length(directdeps))} \
package$(ifelse(length(directdeps) == 1, \"\", \"s\"))}$indirect_info:")
columnlist(io, depstrs)
end
function listdeps(deps::Dict{Base.UUID, Pkg.Types.PackageEntry}, pkg::Base.UUID)
if haskey(deps, pkg)
collect(values(deps[pkg].deps))
else
Base.UUID[]
end
end
function listdeps(deps::Dict{Base.UUID, Pkg.API.PackageInfo}, pkg::Base.UUID)
if haskey(deps, pkg)
collect(values(deps[pkg].dependencies))
else
Base.UUID[]
end
end
function alldeps(deps::Dict{Base.UUID, <:Union{Pkg.Types.PackageEntry, Pkg.API.PackageInfo}}, pkg::Base.UUID)
depcheck = listdeps(deps, pkg)
depcollection = Set{Base.UUID}()
while !isempty(depcheck)
id = popfirst!(depcheck)
id in depcollection && continue
append!(depcheck, listdeps(deps, id))
push!(depcollection, id)
end
collect(depcollection)
end
end

View File

@ -1,9 +1,12 @@
module About
using Base: AnnotatedString, AnnotatedIOBuffer
using StyledStrings: @styled_str, Face, face!
using StyledStrings: @styled_str, Face, face!, addface!
using JuliaSyntaxHighlighting: highlight
using InteractiveUtils
const var"@S_str" = var"@styled_str"
export about
include("utils.jl")
@ -12,12 +15,13 @@ include("types.jl")
include("values.jl")
"""
about(fn::Function, [signature::Tuple])
about(typ::Type)
about(obj::Any)
about([io::IO], fn::Function, [argtypes::Type...])
about([io::IO], typ::Type)
about([io::IO], val::Any)
Display information on the particular nature of the argument, whether
it be a function, type, or value.
Display information on the particular nature of the argument, whatever it may be.
TODO mention the mechanisms for extension here too.
"""
function about end
@ -30,4 +34,38 @@ function about(xs...)
end
end
"""
memorylayout(io::IO, T::DataType)
memorylayout(io::IO, val::T)
Print to `io` the memory layout of the type `T`, or `val` a particular instance
of the type.
Specialised implementations should be implemented freely to enhance the utility
and prettiness of the display.
"""
function memorylayout end
"""
elaboration(::IO, x::Any)
Elaborate on `x` to io, providing extra information that might be of interest
seperately from `about` or `memorylayout`.
Specialised implementations should be implemented freely to enhance the utility
and prettiness of the display.
By convention, this is not invoked when displaying `x` compactly.
"""
elaboration(::IO, ::Any) = nothing
const ABOUT_FACES = [
:about_module => Face(foreground=:bright_red),
:about_pointer => Face(foreground=:cyan),
:about_count => Face(weight=:bold),
:about_bytes => Face(weight=:bold),
]
__init__() = foreach(addface!, ABOUT_FACES)
end

View File

@ -2,11 +2,12 @@ function about(io::IO, fn::Function)
source = Main.InteractiveUtils.which(parentmodule(fn), Symbol(fn))
methodmodules = getproperty.(methods(fn).ms, :module)
others = setdiff(methodmodules, [source])
print(io, Base.summary(fn), styled"\n Defined in {bright_red:$source}")
fn_name, fn_extra = split(Base.summary(fn), ' ', limit=2)
print(io, S"{julia_funcall:$fn_name} $fn_extra\n Defined in {about_module:$source}")
if length(others) > 0
print(io, styled"{shadow:({emphasis:$(sum(Ref(source) .=== methodmodules))})} extended in ")
print(io, S"{shadow:({emphasis:$(sum(Ref(source) .=== methodmodules))})} extended in ")
for (i, oth) in enumerate(others)
print(io, styled"{bright_red:$oth}{shadow:({emphasis:$(sum(Ref(oth) .=== methodmodules))})}")
print(io, S"{about_module:$oth}{shadow:({emphasis:$(sum(Ref(oth) .=== methodmodules))})}")
if length(others) == 2 && i == 1
print(io, " and ")
elseif length(others) > 2 && i < length(others)-1
@ -19,6 +20,23 @@ function about(io::IO, fn::Function)
print(io, ".\n")
end
function about(io::IO, @nospecialize(cfn::ComposedFunction))
print(io, S"{bold:Composed function:} ")
fnstack = Function[]
function decompose!(fnstk, c::ComposedFunction)
decompose!(fnstk, c.outer)
decompose!(fnstk, c.inner)
end
decompose!(fnstk, c::Function) = push!(fnstk, c)
decompose!(fnstack, cfn)
join(io, map(f -> S"{julia_funcall:$f}", fnstack), S" {julia_operator:∘} ")
println(io)
for fn in fnstack
print(io, S" {emphasis:•} ")
about(io, fn)
end
end
function about(io::IO, method::Method)
fn, sig = first(method.sig.types).instance, Tuple{map(Base.unwrap_unionall, method.sig.types[2:end])...}
show(io, method)
@ -26,40 +44,127 @@ function about(io::IO, method::Method)
print_effects(io, fn, sig)
end
function about(io::IO, fn::Function, @nospecialize(sig::Type{<:Tuple}))
function about(io::IO, fn::Function, @nospecialize(argtypes::Type{<:Tuple}))
about(io, fn); println(io)
ms = methods(fn, sig)
ms = methods(fn, argtypes)
if isempty(ms)
print(io, styled" {error:!} No methods matched $fn($(join(collect(Tuple{Int64, Int64, Int64}.types), \", \")))")
fncall = highlight("$fn($(join(collect(argtypes.types), ", ")))")
println(io, S" {error:!} No methods matched $fncall")
return
end
println(io, styled" Matched {emphasis:$(length(ms))} method$(ifelse(length(ms) > 1, \"s\", \"\")):")
for method in ms
println(io, " ", sprint(show, method, context=IOContext(io)))
rinfo = let rtypes = Base.return_types(fn, argtypes) # HACK: this is technically private API
unique!(rtypes)
for i in eachindex(rtypes), j in eachindex(rtypes)
Tᵢ, Tⱼ = rtypes[i], rtypes[j]
if Tᵢ <: Tⱼ
rtypes[i] = Tⱼ
elseif Tⱼ <: Tᵢ
rtypes[j] = Tᵢ
end
end
unique!(rtypes)
sort!(rtypes, by=length supertypes)
join(map(t -> S"{julia_type:$t}", rtypes), ", ")
end
println(io, S" Matched {emphasis:$(length(ms))} method$(ifelse(length(ms) > 1, \"s\", \"\")) {julia_type:::} $rinfo")
for method in ms
mcall, msrc = split(sprint(show, method), " @ ")
msrcinfo = match(r"^([A-Z][A-Za-z0-9\.]+) (.+)$", msrc)
msrcpretty = if isnothing(msrcinfo)
S"{shadow,underline:$msrc}"
else
mmod, mfile = msrcinfo.captures
S"{about_module:$mmod} {shadow,underline:$mfile}"
end
println(io, S" {light:$(highlight(mcall))} {shadow,bold:@} $msrcpretty")
end
println(io)
@static if VERSION >= v"1.8"
about(io, Base.infer_effects(fn, argtypes))
end
print_effects(io, fn, sig)
end
function print_effects(io::IO, fn::Function, @nospecialize(sig::Type{<:Tuple}))
effects = Base.infer_effects(fn, sig)
ATRUE, AFALSE = Core.Compiler.ALWAYS_TRUE, Core.Compiler.ALWAYS_FALSE
echar(t::UInt8) = get(Dict(ATRUE => '✔', AFALSE => '✗'), t, '?')
echar(b::Bool) = ifelse(b, '✔', '✗')
eface(t::UInt8) = get(Dict(ATRUE => :success, AFALSE => :error), t, :warning)
eface(b::Bool) = ifelse(b, :success, :error)
hedge(t::UInt8) = get(Dict(ATRUE => styled"guaranteed to",
AFALSE => styled"{italic:not} guaranteed to"), t,
styled"???")
hedge(b::Bool) = hedge(ifelse(b, ATRUE, AFALSE))
println(io)
for (effect, description) in
[(:consistent, "return or terminate consistently"),
(:effect_free, "be free from externally semantically visible side effects"),
(:nothrow, "never throw an exception"),
(:terminates, "terminate")]
e = getfield(effects, effect)
print(io, styled" {$(eface(e)):{bold:$(echar(e))} $(rpad(effect, 11))} {shadow:$(hedge(e)) $description}")
print('\n')
@static if VERSION >= v"1.8"
struct CompatibleCoreCompilerConstants end
function Base.getproperty(::CompatibleCoreCompilerConstants, name::Symbol)
if isdefined(Core.Compiler, name)
getglobal(Core.Compiler, name)
end
end
const C4 = CompatibleCoreCompilerConstants()
function about(io::IO, effects::Core.Compiler.Effects)
function effectinfo(io::IO, field::Symbol, name::String, labels::Pair{<:Union{UInt8, Bool, Nothing}, AnnotatedString{String}}...;
prefix::AbstractString = "", suffix::AbstractString = "")
hasproperty(effects, field) || return
value = @static if VERSION >= v"1.9"
getproperty(effects, field)
else # v1.8
getproperty(effects, field).state
end
icon, accent = if value == C4.ALWAYS_TRUE || value === true
'✔', :success
elseif value == C4.ALWAYS_FALSE || value === false
'✗', :error
else
'~', :warning
end
msg = S"{bold,italic,grey:???}"
for (id, label) in labels
if id == value
msg = label
break
end
end
name_pad_width = 13
dispwidth = last(displaysize(io))
declr = S" {bold,$accent:$icon $(rpad(name, name_pad_width))} "
print(io, declr)
indent = name_pad_width + 5
desc = S"{grey:$prefix$(ifelse(isempty(prefix), \"\", \" \"))$msg$(ifelse(isempty(suffix), \"\", \" \"))$suffix}"
desclines = wraplines(desc, dispwidth - indent, indent)
for (i, line) in enumerate(desclines)
i > 1 && print(io, ' '^indent)
println(io, line)
end
end
println(io, S"{bold:Method effects:}")
effectinfo(io, :consistent, "consistent",
C4.ALWAYS_TRUE => S"guaranteed to",
C4.ALWAYS_FALSE => S"might {italic:not}",
C4.CONSISTENT_IF_NOTRETURNED => S"when the return value never involved newly allocated mutable objects, will",
C4.CONSISTENT_IF_INACCESSIBLEMEMONLY => S"when {code:inaccessible memory only} is also proven, will",
suffix = "return or terminate consistently")
effectinfo(io, :effect_free, "effect free",
C4.ALWAYS_TRUE => S"guaranteed to be",
C4.ALWAYS_FALSE => S"might {italic:not} be",
C4.EFFECT_FREE_IF_INACCESSIBLEMEMONLY => S"when {code:inaccessible memory only} is also proven, is",
suffix = "free from externally semantically visible side effects")
effectinfo(io, :nothrow, "no throw",
C4.ALWAYS_TRUE => S"guaranteed to never",
C4.ALWAYS_FALSE => S"may",
suffix = "throw an exception")
effectinfo(io, :terminates, "terminates",
C4.ALWAYS_TRUE => S"guaranteed to",
C4.ALWAYS_FALSE => S"might {italic:not}",
suffix = "always terminate")
effectinfo(io, :notaskstate, "no task state",
C4.ALWAYS_TRUE => S"guaranteed not to access task state (allowing migration between tasks)",
C4.ALWAYS_FALSE => S"may access task state (preventing migration between tasks)")
effectinfo(io, :inaccessiblememonly, "inaccessible memory only",
C4.ALWAYS_TRUE => S"guaranteed to never access or modify externally accessible mutable memory",
C4.ALWAYS_FALSE => S"may access or modify externally accessible mutable memory",
C4.INACCESSIBLEMEM_OR_ARGMEMONLY => S"may access or modify mutable memory {italic:iff} pointed to by its call arguments")
effectinfo(io, :noub, "no undefined behaviour",
C4.ALWAYS_TRUE => S"guaranteed to never",
C4.ALWAYS_FALSE => S"may",
C4.NOUB_IF_NOINBOUNDS => S"so long as {code,julia_macro:@inbounds} is not used or propagated, will not",
suffix = "execute undefined behaviour")
effectinfo(io, :nonoverlayed, "non-overlayed",
true => S"never calls any methods from an overlayed method table",
false => S"{warning:may} call methods from an overlayed method table")
end
end

View File

@ -1,5 +1,3 @@
const POINTER_FACE = :cyan # should not appear in `FACE_CYCLE`
struct FieldInfo
i::Int
face::Union{Symbol, Face}
@ -11,19 +9,28 @@ struct FieldInfo
type::Type
end
function structinfo(T::DataType)
function structinfo(T::Type)
map(1:fieldcount(T)) do i
size = Int(if i < fieldcount(T)
fieldoffset(T, i+1)
else
sizeof(T)
end - fieldoffset(T, i))
contentsize = try sizeof(fieldtype(T, i)) catch _ 0 end
if contentsize > size # Pointer?
contentsize = 0
if hassizeof(T)
offset = fieldoffset(T, i) |> Int
size = Int(if i < fieldcount(T)
fieldoffset(T, i+1)
else
sizeof(T)
end - fieldoffset(T, i))
contentsize = if hassizeof(fieldtype(T, i))
sizeof(fieldtype(T, i))
else
0
end
if contentsize > size # Pointer?
contentsize = 0
end
else
offset = size = contentsize = -1 # Cannot deduce easily
end
FieldInfo(i, FACE_CYCLE[i % length(FACE_CYCLE) + 1],
fieldoffset(T, i) |> Int, # offset
offset,
size, contentsize,
contentsize == 0, # ispointer
fieldname(T, i), fieldtype(T, i))
@ -36,7 +43,7 @@ function about(io::IO, type::Type)
elseif isconcretetype(type)
print(io, "Concrete ")
if Base.datatype_haspadding(type)
print(io, styled"{shadow:(padded)} ")
print(io, S"{shadow:(padded)} ")
end
elseif isabstracttype(type)
print(io, "Abstract ")
@ -45,20 +52,22 @@ function about(io::IO, type::Type)
print(io, "singleton ")
end
print(Base.summary(type))
println(io, styled" defined in {bright_red:$(type.name.module)}, $(join(humansize(sizeof(type))))",
"\n ", supertypestr(type))
print(io, S" defined in {about_module:$(parentmodule(type))}, ")
hassizeof(type) && print(io, "$(join(humansize(sizeof(type))))")
print(io, "\n ")
supertypeinfo(io, type)
(!isstructtype(type) || fieldcount(type) == 0) && return
println(io, styled"\nStruct with {bold:$(fieldcount(type))} fields:")
println(io, S"\n\nStruct with {bold:$(fieldcount(type))} fields:")
fieldinfo = AnnotatedString[]
if type isa DataType
sinfo = structinfo(type)
namepad = maximum(fi -> textwidth(string(fi.name)), sinfo) + 1
for (; face, name, type, ispointer) in sinfo
push!(fieldinfo, rpad(styled"{$face:$name}", namepad) * styled"{$POINTER_FACE:$(ifelse(ispointer, \"*\", \" \"))}$type")
push!(fieldinfo, rpad(S"{$face:$name}", namepad) * S"{about_pointer:$(ifelse(ispointer, \"*\", \" \"))}$type")
end
else
for (; name, type) in structinfo(type)
push!(fieldinfo, styled"$name{shadow:::$type}")
push!(fieldinfo, S"$name{shadow:::$type}")
end
end
if length(fieldinfo) < 32
@ -67,15 +76,18 @@ function about(io::IO, type::Type)
columnlist(io, fieldinfo, spacing=3)
end
if type isa DataType
println(io)
memorylayout(io, type)
end
end
supertypestr(type::Type) =
join(string.(supertypes(type)), styled" {red:<:} ")
function supertypeinfo(io::IO, type::Type)
typestr(t) = highlight(sprint(show, Base.unwrap_unionall(t)))
join(io, map(typestr, supertypes(type)),
S" {julia_comparator:<:} ")
end
function memorylayout(io::IO, type::DataType)
hassizeof(type) || return
si = structinfo(type)
!isempty(si) || return
memstep = memstep = gcd((getfield.(si, :size), getfield.(si, :contentsize)) |>
@ -84,33 +96,36 @@ function memorylayout(io::IO, type::DataType)
bars = AnnotatedString[]
descs = AnnotatedString[]
for (; i, size, contentsize, ispointer) in si
size <= 0 && continue
color = FACE_CYCLE[i % length(FACE_CYCLE) + 1]
width = max(2, memscale * size÷memstep)
color = FACE_CYCLE[i % length(FACE_CYCLE) + 1]
fsize, funits = humansize(size)
desc = if ispointer
cpad(styled" {$color:*} ", width)
cpad(S" {$color,bold:*} ", width)
elseif contentsize < size
csize, cunits = humansize(contentsize)
psize, punits = humansize(size - contentsize)
cpad(styled" {$color:$csize$cunits}{shadow:+$psize$punits} ", width, ' ', RoundUp)
cpad(S" {$color:$csize$cunits}{shadow:+$psize$punits} ", width, ' ', RoundUp)
else
cpad(styled" {$color:$fsize$funits} ", width)
cpad(S" {$color:$fsize$funits} ", width)
end
push!(descs, desc)
width = textwidth(desc)
contentwidth = round(Int, width * contentsize / size)
bar = styled"{$color:$('■'^contentwidth)}"
bar = S"{$color:$('■'^contentwidth)}"
if contentsize < size
color = if ispointer; :cyan else :light_black end
paddwidth = width - contentwidth
bar *= styled"{$color:$('■'^paddwidth)}"
if ispointer
bar *= S"{about_pointer,light:$('■'^paddwidth)}"
else
bar *= S"{shadow:$('■'^paddwidth)}"
end
end
push!(bars, bar)
end
multirow_wrap(io, permutedims(hcat(bars, descs)))
if any(getfield.(si, :ispointer))
print(io, styled"\n {$POINTER_FACE:*} = {$POINTER_FACE:Pointer} {light:(8B)}")
end
println(io)
multirow_wrap(io, permutedims(hcat(bars, descs)))
if any(i -> i.ispointer, si)
println(io, S"\n {about_pointer,bold:*} = {about_pointer:Pointer} {light:(8B)}")
end
end

View File

@ -10,10 +10,20 @@ function humansize(bytes::Integer)
end, units[1+magnitude]
end
function hassizeof(type::Type)
!isconcretetype(type) && return false
type <: GenericMemory && return false
type in (Symbol, String, Core.SimpleVector) && return false
true
end
function cpad(s, n::Integer, pad::Union{AbstractString, AbstractChar}=' ', r::RoundingMode = RoundToZero)
rpad(lpad(s, div(n+textwidth(s), 2, r), pad), n, pad)
end
splural(n::Int) = ifelse(n == 1, "", "s")
splural(c::Vector) = splural(length(c))
function struncate(str::AbstractString, maxwidth::Int, joiner::AbstractString = "", mode::Symbol = :center)
textwidth(str) <= maxwidth && return str
left, right = firstindex(str) - 1, lastindex(str) + 1
@ -33,17 +43,19 @@ end
function columnlist(io::IO, entries::Vector{<:AbstractString};
maxcols::Int=8, maxwidth::Int=last(displaysize(io)),
prefix::AbstractString = styled"{emphasis:•} ", spacing::Int=2)
prefix::AbstractString = S"{emphasis:•} ", spacing::Int=2)
isempty(entries) && return
thecolumns = Vector{eltype(entries)}[]
thecolwidths = Int[]
for ncols in 1:maxcols
columns = Vector{eltype(entries)}[]
for col in Iterators.partition(entries, length(entries) ÷ ncols)
for col in Iterators.partition(entries, div(length(entries), ncols, RoundUp))
push!(columns, collect(col))
end
widths = map.(textwidth, columns)
colwidths = map(maximum, widths)
if sum(colwidths) + ncols * textwidth(prefix) + (1 - ncols) * spacing > maxwidth
layoutwidth = sum(colwidths) + ncols * textwidth(prefix) + (ncols - 1) * spacing
if layoutwidth > maxwidth
break
else
thecolumns, thecolwidths = columns, colwidths
@ -81,3 +93,45 @@ function multirow_wrap(io::IO, cells::Matrix{<:AbstractString};
end
end
end
"""
wraplines(content::AnnotatedString, width::Integer = 80, column::Integer = 0)
Wrap `content` into a vector of lines of at most `width` (according to
`textwidth`), with the first line starting at `column`.
"""
function wraplines(content::Union{Annot, SubString{<:Annot}}, width::Integer = 80, column::Integer = 0) where { Annot <: AnnotatedString}
s, lines = String(content), SubString{Annot}[]
i, lastwrap, slen = firstindex(s), 0, ncodeunits(s)
most_recent_break_opportunity = 1
while i < slen
if isspace(s[i]) && s[i] != '\n'
most_recent_break_opportunity = i
elseif s[i] == '\n'
push!(lines, content[nextind(s, lastwrap):prevind(s, i)])
lastwrap = i
column = 0
elseif column >= width && most_recent_break_opportunity > 1
if lastwrap == most_recent_break_opportunity
nextbreak = findfirst(isspace, @view s[nextind(s, lastwrap):end])
if isnothing(nextbreak)
break
else
most_recent_break_opportunity = lastwrap + nextbreak
end
i = most_recent_break_opportunity
else
i = nextind(s, most_recent_break_opportunity)
end
push!(lines, content[nextind(s, lastwrap):prevind(s, most_recent_break_opportunity)])
lastwrap = most_recent_break_opportunity
column = 0
end
column += textwidth(s[i])
i = nextind(s, i)
end
if lastwrap < slen
push!(lines, content[nextind(s, lastwrap):end])
end
lines
end

View File

@ -1,21 +1,49 @@
const NUMBER_BIT_FACES = (
sign = :bright_blue,
exponent = :bright_green,
mantissa = :bright_red
)
# ------------------
# Structs, in general
# ------------------
function about(io::IO, value::T) where {T}
print(io, Base.summary(value))
ismutable(value) && print(io, " (mutable)")
directbytes = sizeof(value)
indirectbytes = Base.summarysize(value)
print(io, styled", {bold:$(join(humansize(directbytes)))}")
if indirectbytes > directbytes
print(io, styled" referencing {bold:$(join(humansize(indirectbytes)))}")
# Type information
iotype = AnnotatedIOBuffer()
print(iotype, Base.summary(value))
ismutable(value) && print(iotype, " (mutable)")
print(iotype, S" ({julia_comparator:<:} ")
supertypeinfo(iotype, supertype(T))
print(iotype, ")")
infotype = read(seekstart(iotype), AnnotatedString)
# Size information
typesize = try sizeof(T) catch _ sizeof(value) end
datasize = sizeof(value)
netsize = Base.summarysize(value)
infosize = if typesize == datasize == netsize
S"{about_bytes:$(join(humansize(typesize)))}."
elseif typesize == datasize <= netsize
S"{about_bytes:$(join(humansize(typesize)))} directly \
(referencing {about_bytes:$(join(humansize(netsize)))} in total)"
elseif typesize == datasize > netsize
S"{about_bytes:$(join(humansize(typesize)))} directly \
({warning:!} referencing {about_bytes:$(join(humansize(netsize)))} in total, \
{warning:strangely less than the direct, \
{underline,link={https://github.com/tecosaur/About.jl}:\
please open an issue on About.jl with this example}})"
else # all different
S"{about_bytes:$(join(humansize(typesize)))} directly \
(referencing {about_bytes:$(join(humansize(netsize)))} in total, \
holding {about_bytes:$(join(humansize(datasize)))} of data)"
end
print(io, styled" ({red:<:} ", supertypestr(supertype(T)), ")")
println(io)
print(io, infotype)
if textwidth(infotype) < last(displaysize(io)) &&
textwidth(infotype) + textwidth(infosize) + 12 >= last(displaysize(io))
print(io, "\n Memory footprint: ")
else
print(io, ", occupies ")
end
println(io, infosize)
# Layout + elaboration
memorylayout(io, value)
if get(io, :compact, false) != true
elaboration(io, value)
end
end
function memorylayout(io::IO, value::T) where {T}
@ -28,7 +56,12 @@ function memorylayout(io::IO, value::T) where {T}
print(io, "«struct»")
return
end
if Base.issingletontype(T)
println(io, S"{italic:singelton}")
return
end
sinfo = structinfo(T)
isempty(sinfo) && return
ffaces = Union{Face, Symbol}[]
fnames = String[]
ftypes = String[]
@ -41,19 +74,24 @@ function memorylayout(io::IO, value::T) where {T}
push!(ftypes, string(type))
push!(fsizes, join(humansize(size)))
aio = AnnotatedIOBuffer()
if ispointer
fvalue = getfield(value, name)
if Base.issingletontype(typeof(fvalue))
push!(freprs, S"{shadow:singleton}")
elseif size == 0
push!(freprs, S"{error:??}")
elseif ispointer
try
pt = pointer(getfield(value, name))
push!(freprs, styled"{$POINTER_FACE,light:$pt}")
pt = pointer(fvalue)
push!(freprs, S"{about_pointer:@ $(sprint(show, UInt64(pt)))}")
catch
push!(freprs, styled"{$POINTER_FACE:Ptr?}")
push!(freprs, S"{about_pointer:Ptr?}")
end
else
memorylayout(IOContext(aio, :compact => true), getfield(value, name))
memorylayout(IOContext(aio, :compact => true), fvalue)
push!(freprs, read(seekstart(aio), AnnotatedString))
end
truncate(aio, 0)
show(IOContext(aio, :compact => true), getfield(value, name))
show(IOContext(aio, :compact => true), fvalue)
push!(fshows, read(seekstart(aio), AnnotatedString))
end
width = last(displaysize(io)) - 2
@ -65,14 +103,99 @@ function memorylayout(io::IO, value::T) where {T}
showwidth = width - reprwidth
for (face, name, type, size, brepr, shown) in zip(ffaces, fnames, ftypes, fsizes, freprs, fshows)
println(io, ' ',
styled"{$face:$(lpad(name, namewidth)){shadow:::}$(rpad(struncate(type, typewidth, \"\", :right), typewidth)) $(lpad(size, sizewidth))}",
' ', rpad(struncate(brepr, reprwidth, styled" {shadow:…} "), reprwidth),
' ', face!(struncate(shown, showwidth, styled" {shadow:…} "), face))
S"{$face:$(lpad(name, namewidth)){shadow:::}$(rpad(struncate(type, typewidth, \"\", :right), typewidth)) $(lpad(size, sizewidth))}",
' ', rpad(struncate(brepr, reprwidth, S" {shadow:…} "), reprwidth),
' ', face!(struncate(shown, showwidth, S" {shadow:…} "), face))
end
println(io)
memorylayout(io, T)
end
# ------------------
# Modules
# ------------------
function about(io::IO, mod::Module)
pkg = nothing
for (bpkg, m) in Base.loaded_modules
if m == mod
pkg = bpkg
break
end
end
!isnothing(pkg) && !applicable(about_pkg, io, pkg, mod) &&
Base.require(Base.PkgId(Base.UUID("44cfe95a-1eb2-52ea-b672-e2afdf69b78f"), "Pkg"))
print(io, S"{bold:Module {about_module:$mod}}")
if !isnothing(pkg)
println(io, S" {shadow:[$(something(pkg.uuid, \"no uuid\"))]}")
Base.invokelatest(about_pkg, io, pkg, mod)
else
println(io)
end
function classify(m::Module, name::Symbol)
val = getglobal(mod, name)
order, kind, face, parent = if val isa Module
0, :module, :about_module, val
elseif val isa Function && first(String(name)) == '@'
1, :macro, :julia_macro, parentmodule(val)
elseif val isa Function
2, :function, :julia_funcall, parentmodule(val)
elseif val isa Type
3, :type, :julia_type, if val isa UnionAll || val isa Union
m else parentmodule(val) end
else
4, :value, :julia_identifier, if Base.issingletontype(typeof(val))
parentmodule(typeof(m))
else
m
end
end
while parentmodule(parent) (parent, Main)
parent = parentmodule(parent)
end
(; name, str = S"{code,$face:$name}", kind, parent, order)
end
classify(m::Module, names::Vector{Symbol}) =
sort(map(Base.Fix1(classify, m), names), by=x->x.order)
allnames = classify(mod, names(mod))
exports = similar(allnames, 0)
reexports = similar(allnames, 0)
publics = similar(allnames, 0)
for exp in allnames
if exp.parent === mod && Base.isexported(mod, exp.name)
push!(exports, exp)
elseif exp.parent === mod && Base.ispublic(mod, exp.name)
push!(publics, exp)
elseif exp.parent !== mod
push!(reexports, exp)
end
end
if !isempty(exports)
println(io, S"\n{bold:Exports {emphasis:$(length(exports))} name$(splural(exports)):}")
columnlist(io, map(x->x.str, exports))
end
if !isempty(reexports)
parents = join(sort(map(p->S"{about_module:$p}", unique(map(x->x.parent, reexports)))), ", ")
println(io, S"\n{bold:Re-exports {emphasis:$(length(reexports))} name$(splural(reexports))} (from $parents){bold::}")
columnlist(io, map(x->x.str, reexports))
end
if !isempty(publics)
println(io, S"\n{bold:Public API ({emphasis:$(length(publics))} name$(splural(publics))):}")
columnlist(io, map(x->x.str, publics))
end
end
function about_pkg end # Implemented in `../ext/PkgExt.jl`
# ------------------
# Numeric types
# ------------------
const NUMBER_BIT_FACES = (
sign = :bright_blue,
exponent = :bright_green,
mantissa = :bright_red
)
function memorylayout(io::IO, value::Bool)
bits = AnnotatedString(bitstring(value))
face!(bits[1:end-1], :shadow)
@ -80,7 +203,7 @@ function memorylayout(io::IO, value::Bool)
if get(io, :compact, false) == true
print(io, bits)
else
println(io, "\n ", bits, styled" {bold:=} $value")
println(io, "\n ", bits, S" {bold:=} $value")
end
end
@ -93,7 +216,7 @@ function memorylayout(io::IO, value::Union{UInt8, UInt16, UInt32, UInt64, UInt12
print(io, bits)
else
println(io, "\n ", bits, ifelse(sizeof(value) > 4, "\n", ""),
styled" {bold:=} $value")
S" {bold:=} $value")
end
end
@ -111,7 +234,7 @@ function memorylayout(io::IO, value::Union{Int8, Int16, Int32, Int64, Int128})
else
signstr = ifelse(value < 0, '-', '+')
println(io, "\n ", bits, ifelse(sizeof(value) > 4, "\n", ""),
styled" {bold:=} {$(NUMBER_BIT_FACES.sign):$signstr}$value")
S" {bold:=} {$(NUMBER_BIT_FACES.sign):$signstr}$(abs(value))")
end
end
@ -123,7 +246,7 @@ memorylayout(io::IO, float::Core.BFloat16) = floatlayout(io, float, 8)
function floatlayout(io::IO, float::AbstractFloat, expbits::Int)
fsign, fexp, fmant = NUMBER_BIT_FACES.sign, NUMBER_BIT_FACES.exponent, NUMBER_BIT_FACES.mantissa
bitstr = bitstring(float)
hl_bits = styled"{$fsign:$(bitstr[1])}{$fexp:$(bitstr[2:expbits+1])}{$fmant:$(bitstr[expbits+2:end])}"
hl_bits = S"{$fsign:$(bitstr[1])}{$fexp:$(bitstr[2:expbits+1])}{$fmant:$(bitstr[expbits+2:end])}"
if get(io, :compact, false) == true
print(io, hl_bits)
else
@ -147,28 +270,101 @@ function floatlayout(io::IO, float::AbstractFloat, expbits::Int)
eright = (expbits - 3) - eleft
fleft = (fracbits - 3) ÷ 2
fright = (fracbits - 3) - fleft
styled"{$fsign:╨}{$fexp:└$('─'^eleft)$('─'^eright)┘}{$fmant:└$('─'^fleft)$('─'^fright)┘}"
S"{$fsign:╨}{$fexp:└$('─'^eleft)$('─'^eright)┘}{$fmant:└$('─'^fleft)$('─'^fright)┘}"
end
hl_vals = styled"{$fsign,bold:$sign}{$fexp:$expstr}{bold:×}{$fmant:$fracstr}"
hl_more = styled" {$fexp:exponent}$(' '^17){$fmant:mantissa / fraction}"
hl_vals = S"{$fsign,bold:$sign}{$fexp:$expstr}{bold:×}{$fmant:$fracstr}"
hl_more = S" {$fexp:exponent}$(' '^17){$fmant:mantissa / fraction}"
println(io, "\n ", hl_bits, " \n ", hl_info, "\n ", hl_vals,
styled"\n {bold:=} ", if -8 < exponent < 8
S"\n {bold:=} ", if -8 < exponent < 8
Base.Ryu.writefixed(float, fracdp)
else Base.Ryu.writeexp(float, fracdp) end)
end
end
# ------------------
# Char/String
# ------------------
function memorylayout(io::IO, char::Char)
chunks = reinterpret(NTuple{4, UInt8}, reinterpret(UInt32, char) |> hton)
get(io, :compact, false) || print(io, "\n ")
for chunk in chunks
cstr = AnnotatedString(bitstring(chunk))
if iszero(chunk)
face!(cstr, :shadow)
else
for (; match) in eachmatch(r"1+", cstr)
face!(match, :bright_green)
nchunks = something(findlast(!iszero, chunks), 1)
byte0leading = [1, 3, 4, 5][nchunks]
ucodepoint = if Base.isoverlong(char)
Base.decode_overlong(char)
else
codepoint(char)
end
bit_spreads =
[[3, 4],
[3, 4, 4],
[4, 4, 4, 4],
[1, 4, 4, 4, 4, 4]
][nchunks]
ubytes = collect(uppercase(string(
ucodepoint, base=16, pad = length(bit_spreads))))
overlong_bytes = if Base.isoverlong(char)
1:min(something(findfirst(==('1'), ubytes), length(ubytes)) - 1,
length(ubytes) - 2)
else 1:0 end
chunk_coloring = [Pair{UnitRange{Int}, Symbol}[] for _ in 1:length(chunks)]
ustr = S"{bold:U+$(lpad(join(ubytes), 4, '0'))}"
for (i, b, color) in zip(1:length(ubytes),
collect(eachindex(ustr))[end-length(ubytes)+1:end],
Iterators.cycle(Iterators.reverse(FACE_CYCLE)))
if i in overlong_bytes
color = :error # overlong
end
face!(ustr[b:b], color)
end
if get(io, :compact, false) == true
print(io, ustr, ' ')
else let
current_bit = byte0leading
print(io, ' '^byte0leading)
for (i, ubyte, nbits, color) in zip(1:length(ubytes), ubytes, bit_spreads,
Iterators.cycle(Iterators.reverse(FACE_CYCLE)))
if i in overlong_bytes
color = :error # overlong
end
does_byte_jump = current_bit ÷ 8 < (current_bit + nbits) ÷ 8
clean_jump = does_byte_jump && (current_bit + nbits) % 8 == 0
next_bit = current_bit + nbits + does_byte_jump * 2
width = nbits + 3 * (does_byte_jump && !clean_jump)
byte_brace = if width <= 2
lpad(ubyte, width)
else
'┌' * cpad(ubyte, width-2, '─') * '┐'
end
print(io, S"{$color:$byte_brace}")
clean_jump && print(io, " ")
if does_byte_jump && !clean_jump
push!(chunk_coloring[1 + current_bit ÷ 8], (1 + current_bit % 8):8 => color)
push!(chunk_coloring[1 + next_bit ÷ 8], 3:mod1(next_bit, 8) => color)
else
push!(chunk_coloring[1 + current_bit ÷ 8],
(1 + current_bit % 8):mod1(current_bit + nbits, 8) => color)
end
current_bit = next_bit
end
print(io, "\n ")
end end
for (i, (chunk, coloring)) in enumerate(zip(chunks, chunk_coloring))
cbits = bitstring(chunk)
cstr = if i > nchunks
S"{shadow:$cbits}"
else
leadingbits = if i == 1; byte0leading else 2 end
leading = cbits[1:leadingbits]
rest = AnnotatedString(cbits[leadingbits+1:end])
for (; match) in eachmatch(r"1+", rest)
face!(match, :underline)
end
cstr = S"{shadow:$leading}$rest"
for (range, color) in coloring
face!(cstr, range, color)
end
cstr
end
print(io, cstr, ' ')
end
@ -176,12 +372,86 @@ function memorylayout(io::IO, char::Char)
println(io)
for chunk in chunks
byte = lpad(string(chunk, base=16), 2, '0')
print(io, styled" {shadow:└─0x$(byte)─┘}")
print(io, S" {shadow:└─0x$(byte)─┘}")
end
print(io, "\n = ", ustr)
Base.isoverlong(char) && print(io, S" {error:[overlong]}")
println(io)
end
end
# TODO char
const CONTROL_CHARACTERS =
('\x00' => ("NULL", "Null character", "Originally the code of blank paper tape and used as padding to slow transmission. Now often used to indicate the end of a string in C-like languages."),
'\x01' => ("SOH", "Start of Heading", ""),
'\x02' => ("SOT", "Start of Text", ""),
'\x03' => ("ETX", "End of Text", ""),
'\x04' => ("EOT", "End of Transmission", ""),
'\x05' => ("ENQ", "Enquiry", "Trigger a response at the receiving end, to see if it is still present."),
'\x06' => ("ACK", "Acknowledge", "Indication of successful receipt of a message."),
'\x07' => ("BEL", "Bell", "Call for attention from an operator."),
'\x08' => ("HBS", "Backspace", "Move one position leftwards. Next character may overprint or replace the character that was there."),
'\x09' => ("HT", "Horizontal Tab", "Move right to the next tab stop."),
'\x0a' => ("LF", "Line Feed", "Move down to the same position on the next line (some devices also moved to the left column)."),
'\x0b' => ("VT", "Vertical Tab", "Move down to the next vertical tab stop. "),
'\x0c' => ("FF", "Form Feed", "Move down to the top of the next page. "),
'\x0d' => ("CR", "Carriage Return", "Move to column zero while staying on the same line."),
'\x0e' => ("SO", "Shift Out", "Switch to an alternative character set."),
'\x0f' => ("SI", "Shift In", "Return to regular character set after SO."),
'\x10' => ("DLE", "Data Link Escape", "Cause a limited number of contiguously following characters to be interpreted in some different way."),
'\x11' => ("DC1", "Device Control One (XON)", "Used by teletype devices for the paper tape reader and tape punch. Became the de-facto standard for software flow control, now obsolete."),
'\x12' => ("DC2", "Device Control Two", "Used by teletype devices for the paper tape reader and tape punch. Became the de-facto standard for software flow control, now obsolete."),
'\x13' => ("DC3", "Device Control Three (XOFF)", "Used by teletype devices for the paper tape reader and tape punch. Became the de-facto standard for software flow control, now obsolete."),
'\x14' => ("DC4", "Device Control Four", "Used by teletype devices for the paper tape reader and tape punch. Became the de-facto standard for software flow control, now obsolete."),
'\x15' => ("NAK", "Negative Acknowledge", "Negative response to a sender, such as a detected error. "),
'\x16' => ("SYN", "Synchronous Idle", "A transmission control character used by a synchronous transmission system in the absence of any other character (idle condition) to provide a signal from which synchronism may be achieved or retained between data terminal equipment."),
'\x17' => ("ETB", "End of Transmission Block", "End of a transmission block of data when data are divided into such blocks for transmission purposes."),
'\x18' => ("CAN", "Cancel", "A character, or the first character of a sequence, indicating that the data preceding it is in error. As a result, this data is to be ignored. The specific meaning of this character must be defined for each application and/or between sender and recipient."),
'\x19' => ("EM", "End of Medium", "Indicates on paper or magnetic tapes that the end of the usable portion of the tape had been reached."),
'\x1a' => ("SUB", "Substitute/Control-Z", "A control character used in the place of a character that has been found to be invalid or in error. SUB is intended to be introduced by automatic means."),
'\x1b' => ("ESC", "Escape", "A control character which is used to provide additional control functions. It alters the meaning of a limited number of contiguously following bit combinations. The use of this character is specified in ISO-2022."),
'\x1c' => ("FS", "File Separator", "Used to separate and qualify data logically; its specific meaning has to be specified for each application. If this character is used in hierarchical order, it delimits a data item called a file. "),
'\x1d' => ("GS", "Group Separator", "Used to separate and qualify data logically; its specific meaning has to be specified for each application. If this character is used in hierarchical order, it delimits a data item called a group."),
'\x1e' => ("RG", "Record Separator", "Used to separate and qualify data logically; its specific meaning has to be specified for each application. If this character is used in hierarchical order, it delimits a data item called a record."),
'\x1f' => ("US", "Unit Separator", "Used to separate and qualify data logically; its specific meaning has to be specified for each application. If this character is used in hierarchical order, it delimits a data item called a unit."),
'\x7f' => ("DEL", "Delete", "Originally used to delete characters on punched tape by punching out all the holes."))
function elaboration(io::IO, char::Char)
c0index = findfirst(c -> first(c) == char, CONTROL_CHARACTERS)
stychr = S"{julia_char:$(sprint(show, char))}"
if !isnothing(c0index)
cshort, cname, cinfo = last(CONTROL_CHARACTERS[c0index])
println(io, "\n Control character ", stychr, ": ", cname, " ($cshort)",
ifelse(isempty(cinfo), "", "\n "), cinfo)
elseif isascii(char)
kind = if char in 'a':'z'
"lowercase letter"
elseif char in 'A':'Z'
"uppercase letter"
elseif char in '0':'9'
"numeral"
elseif char == ' '
"space"
elseif char in ('(', ')', '[', ']', '{', '}', '«', '»')
"parenthesis"
elseif char in ('!':'/'..., ':':'@'..., '\\', '^', '_', '`', '|', '~')
"punctuation"
end
println(io, "\n ASCII $kind ", stychr)
elseif char in ('Ç':'ø'..., 'Ø', 'á':'Ñ'..., 'Á':'À', 'ã', 'Ã', 'ð':'Ï'..., 'Ó':'Ý')
println(io, "\n Extended ASCII accented letter ", stychr,
S" ({julia_number:0x$(string(UInt8(char), base=16))})")
elseif Base.isoverlong(char)
elseif codepoint(char) in 128:255
println(io, "\n Extended ASCII symbol ", stychr,
S" ({shadow:0x$(string(Int(char), base=16))})")
else
catstr = Base.Unicode.category_string(char)
catabr = Base.Unicode.category_abbrev(char)
println(io, S"\n Unicode $stychr, category: $catstr ($catabr)")
end
end
end
end
# TODO struct