Initial commit
This commit is contained in:
commit
ee717d4ee9
|
@ -0,0 +1 @@
|
|||
/Manifest.toml
|
|
@ -0,0 +1,12 @@
|
|||
name = "About"
|
||||
uuid = "69d22d85-9f48-4c46-bbbe-7ad8341ff72a"
|
||||
authors = ["TEC <git@tecosaur.net>"]
|
||||
version = "0.1.0"
|
||||
|
||||
[deps]
|
||||
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
|
||||
StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b"
|
||||
|
||||
[compat]
|
||||
InteractiveUtils = "1.11.0"
|
||||
StyledStrings = "1.11.0"
|
|
@ -0,0 +1,33 @@
|
|||
module About
|
||||
|
||||
using Base: AnnotatedString, AnnotatedIOBuffer
|
||||
using StyledStrings: @styled_str, Face, face!
|
||||
using InteractiveUtils
|
||||
|
||||
export about
|
||||
|
||||
include("utils.jl")
|
||||
include("functions.jl")
|
||||
include("types.jl")
|
||||
include("values.jl")
|
||||
|
||||
"""
|
||||
about(fn::Function, [signature::Tuple])
|
||||
about(typ::Type)
|
||||
about(obj::Any)
|
||||
|
||||
Display information on the particular nature of the argument, whether
|
||||
it be a function, type, or value.
|
||||
"""
|
||||
function about end
|
||||
|
||||
about(x) = about(stderr, x)
|
||||
function about(xs...)
|
||||
if first(xs) == stderr
|
||||
throw(MethodError(about, xs))
|
||||
else
|
||||
about(stderr, xs...)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,67 @@
|
|||
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}")
|
||||
if length(others) > 0
|
||||
print(io, styled"{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))})}")
|
||||
if length(others) == 2 && i == 1
|
||||
print(io, " and ")
|
||||
elseif length(others) > 2 && i < length(others)-1
|
||||
print(io, ", ")
|
||||
elseif length(others) > 2 && i == length(others)-1
|
||||
print(io, ", and ")
|
||||
end
|
||||
end
|
||||
end
|
||||
print(io, ".\n")
|
||||
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)
|
||||
println(io)
|
||||
print_effects(io, fn, sig)
|
||||
end
|
||||
|
||||
function about(io::IO, fn::Function, @nospecialize(sig::Type{<:Tuple}))
|
||||
about(io, fn); println(io)
|
||||
ms = methods(fn, sig)
|
||||
if isempty(ms)
|
||||
print(io, styled" {error:!} No methods matched $fn($(join(collect(Tuple{Int64, Int64, Int64}.types), \", \")))")
|
||||
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)))
|
||||
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')
|
||||
end
|
||||
end
|
||||
|
||||
about(io::IO, fn::Function, sig::NTuple{N, <:Type}) where {N} = about(io, fn, Tuple{sig...})
|
||||
about(io::IO, fn::Function, sig::Type...) = about(io, fn, sig)
|
|
@ -0,0 +1,116 @@
|
|||
const POINTER_FACE = :cyan # should not appear in `FACE_CYCLE`
|
||||
|
||||
struct FieldInfo
|
||||
i::Int
|
||||
face::Union{Symbol, Face}
|
||||
offset::Int
|
||||
size::Int
|
||||
contentsize::Int
|
||||
ispointer::Bool
|
||||
name::Union{Symbol, Int}
|
||||
type::Type
|
||||
end
|
||||
|
||||
function structinfo(T::DataType)
|
||||
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
|
||||
end
|
||||
FieldInfo(i, FACE_CYCLE[i % length(FACE_CYCLE) + 1],
|
||||
fieldoffset(T, i) |> Int, # offset
|
||||
size, contentsize,
|
||||
contentsize == 0, # ispointer
|
||||
fieldname(T, i), fieldtype(T, i))
|
||||
end
|
||||
end
|
||||
|
||||
function about(io::IO, type::Type)
|
||||
if isprimitivetype(type)
|
||||
print(io, "Primitive ")
|
||||
elseif isconcretetype(type)
|
||||
print(io, "Concrete ")
|
||||
if Base.datatype_haspadding(type)
|
||||
print(io, styled"{shadow:(padded)} ")
|
||||
end
|
||||
elseif isabstracttype(type)
|
||||
print(io, "Abstract ")
|
||||
end
|
||||
if Base.issingletontype(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))
|
||||
(!isstructtype(type) || fieldcount(type) == 0) && return
|
||||
println(io, styled"\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")
|
||||
end
|
||||
else
|
||||
for (; name, type) in structinfo(type)
|
||||
push!(fieldinfo, styled"$name{shadow:::$type}")
|
||||
end
|
||||
end
|
||||
if length(fieldinfo) < 32
|
||||
columnlist(io, fieldinfo, maxcols=1)
|
||||
else
|
||||
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 memorylayout(io::IO, type::DataType)
|
||||
si = structinfo(type)
|
||||
!isempty(si) || return
|
||||
memstep = memstep = gcd((getfield.(si, :size), getfield.(si, :contentsize)) |>
|
||||
Iterators.flatten |> collect)
|
||||
memscale = max(1, floor(Int, 70/(sizeof(type)/memstep)))
|
||||
bars = AnnotatedString[]
|
||||
descs = AnnotatedString[]
|
||||
for (; i, size, contentsize, ispointer) in si
|
||||
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)
|
||||
elseif contentsize < size
|
||||
csize, cunits = humansize(contentsize)
|
||||
psize, punits = humansize(size - contentsize)
|
||||
cpad(styled" {$color:$csize$cunits}{shadow:+$psize$punits} ", width, ' ', RoundUp)
|
||||
else
|
||||
cpad(styled" {$color:$fsize$funits} ", width)
|
||||
end
|
||||
push!(descs, desc)
|
||||
width = textwidth(desc)
|
||||
contentwidth = round(Int, width * contentsize / size)
|
||||
bar = styled"{$color:$('■'^contentwidth)}"
|
||||
if contentsize < size
|
||||
color = if ispointer; :cyan else :light_black end
|
||||
paddwidth = width - contentwidth
|
||||
bar *= styled"{$color:$('■'^paddwidth)}"
|
||||
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)
|
||||
end
|
|
@ -0,0 +1,83 @@
|
|||
const FACE_CYCLE = [:bright_blue, :bright_green, :bright_yellow, :bright_magenta]
|
||||
|
||||
function humansize(bytes::Integer)
|
||||
units = ("B", "kB", "MB", "GB")
|
||||
magnitude = floor(Int, log(1024, 1 + bytes))
|
||||
if 10 < bytes < 10*1024^magnitude
|
||||
round(bytes / 1024^magnitude, digits=1)
|
||||
else
|
||||
round(Int, bytes / 1024^magnitude)
|
||||
end, units[1+magnitude]
|
||||
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
|
||||
|
||||
function struncate(str::AbstractString, maxwidth::Int, joiner::AbstractString = "…", mode::Symbol = :center)
|
||||
textwidth(str) <= maxwidth && return str
|
||||
left, right = firstindex(str) - 1, lastindex(str) + 1
|
||||
width = textwidth(joiner)
|
||||
while width < maxwidth
|
||||
if mode ∈ (:right, :center)
|
||||
left = nextind(str, left)
|
||||
width += textwidth(str[left])
|
||||
end
|
||||
if mode ∈ (:left, :center) && width < maxwidth
|
||||
right = prevind(str, right)
|
||||
width += textwidth(str[right])
|
||||
end
|
||||
end
|
||||
str[begin:left] * joiner * str[right:end]
|
||||
end
|
||||
|
||||
function columnlist(io::IO, entries::Vector{<:AbstractString};
|
||||
maxcols::Int=8, maxwidth::Int=last(displaysize(io)),
|
||||
prefix::AbstractString = styled"{emphasis:•} ", spacing::Int=2)
|
||||
thecolumns = Vector{eltype(entries)}[]
|
||||
thecolwidths = Int[]
|
||||
for ncols in 1:maxcols
|
||||
columns = Vector{eltype(entries)}[]
|
||||
for col in Iterators.partition(entries, length(entries) ÷ ncols)
|
||||
push!(columns, collect(col))
|
||||
end
|
||||
widths = map.(textwidth, columns)
|
||||
colwidths = map(maximum, widths)
|
||||
if sum(colwidths) + ncols * textwidth(prefix) + (1 - ncols) * spacing > maxwidth
|
||||
break
|
||||
else
|
||||
thecolumns, thecolwidths = columns, colwidths
|
||||
end
|
||||
end
|
||||
for rnum in 1:length(first(thecolumns))
|
||||
for cnum in 1:length(thecolumns)
|
||||
rnum > length(thecolumns[cnum]) && continue
|
||||
cnum > 1 && print(io, ' '^spacing)
|
||||
print(io, prefix, rpad(thecolumns[cnum][rnum], thecolwidths[cnum]))
|
||||
end
|
||||
println(io)
|
||||
end
|
||||
end
|
||||
|
||||
function multirow_wrap(io::IO, cells::Matrix{<:AbstractString};
|
||||
indent::AbstractString = " ", maxwidth::Int=last(displaysize(io)))
|
||||
widths = map(textwidth, cells)
|
||||
colwidths = maximum(widths, dims=1)
|
||||
thiscol = textwidth(indent)
|
||||
segments = UnitRange{Int}[1:0]
|
||||
for (i, (col, width)) in enumerate(zip(eachcol(cells), colwidths))
|
||||
if thiscol + width > maxwidth
|
||||
push!(segments, last(last(segments))+1:i-1)
|
||||
thiscol = textwidth(indent) + width
|
||||
else
|
||||
thiscol += width
|
||||
end
|
||||
end
|
||||
push!(segments, last(last(segments))+1:size(cells, 2))
|
||||
filter!(!isempty, segments)
|
||||
for segment in segments
|
||||
for row in eachrow(cells[:, segment])
|
||||
println(io, indent, join(row))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,187 @@
|
|||
const NUMBER_BIT_FACES = (
|
||||
sign = :bright_blue,
|
||||
exponent = :bright_green,
|
||||
mantissa = :bright_red
|
||||
)
|
||||
|
||||
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)))}")
|
||||
end
|
||||
print(io, styled" ({red:<:} ", supertypestr(supertype(T)), ")")
|
||||
println(io)
|
||||
memorylayout(io, value)
|
||||
end
|
||||
|
||||
function memorylayout(io::IO, value::T) where {T}
|
||||
if isprimitivetype(T)
|
||||
get(io, :compact, false) || print(io, "\n ")
|
||||
print(io, bitstring(value))
|
||||
return
|
||||
end
|
||||
if get(io, :compact, false) == true
|
||||
print(io, "«struct»")
|
||||
return
|
||||
end
|
||||
sinfo = structinfo(T)
|
||||
ffaces = Union{Face, Symbol}[]
|
||||
fnames = String[]
|
||||
ftypes = String[]
|
||||
fsizes = String[]
|
||||
freprs = AnnotatedString[]
|
||||
fshows = AnnotatedString[]
|
||||
for (; face, name, type, size, ispointer) in sinfo
|
||||
push!(ffaces, face)
|
||||
push!(fnames, string(name))
|
||||
push!(ftypes, string(type))
|
||||
push!(fsizes, join(humansize(size)))
|
||||
aio = AnnotatedIOBuffer()
|
||||
if ispointer
|
||||
try
|
||||
pt = pointer(getfield(value, name))
|
||||
push!(freprs, styled"{$POINTER_FACE,light:$pt}")
|
||||
catch
|
||||
push!(freprs, styled"{$POINTER_FACE:Ptr?}")
|
||||
end
|
||||
else
|
||||
memorylayout(IOContext(aio, :compact => true), getfield(value, name))
|
||||
push!(freprs, read(seekstart(aio), AnnotatedString))
|
||||
end
|
||||
truncate(aio, 0)
|
||||
show(IOContext(aio, :compact => true), getfield(value, name))
|
||||
push!(fshows, read(seekstart(aio), AnnotatedString))
|
||||
end
|
||||
width = last(displaysize(io)) - 2
|
||||
namewidth = maximum(textwidth, fnames)
|
||||
typewidth = min(maximum(textwidth, ftypes), width ÷ 4)
|
||||
sizewidth = maximum(textwidth, fsizes)
|
||||
width -= 1 + namewidth + 1 + typewidth + 2 + sizewidth
|
||||
reprwidth = min((2 * width) ÷ 3, maximum(textwidth, freprs))
|
||||
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))
|
||||
end
|
||||
println(io)
|
||||
memorylayout(io, T)
|
||||
end
|
||||
|
||||
function memorylayout(io::IO, value::Bool)
|
||||
bits = AnnotatedString(bitstring(value))
|
||||
face!(bits[1:end-1], :shadow)
|
||||
face!(bits[end:end], NUMBER_BIT_FACES.sign)
|
||||
if get(io, :compact, false) == true
|
||||
print(io, bits)
|
||||
else
|
||||
println(io, "\n ", bits, styled" {bold:=} $value")
|
||||
end
|
||||
end
|
||||
|
||||
function memorylayout(io::IO, value::Union{UInt8, UInt16, UInt32, UInt64, UInt128})
|
||||
bits = AnnotatedString(bitstring(value))
|
||||
for (; match) in eachmatch(r"0+", bits)
|
||||
face!(match, :shadow)
|
||||
end
|
||||
if get(io, :compact, false) == true
|
||||
print(io, bits)
|
||||
else
|
||||
println(io, "\n ", bits, ifelse(sizeof(value) > 4, "\n", ""),
|
||||
styled" {bold:=} $value")
|
||||
end
|
||||
end
|
||||
|
||||
function memorylayout(io::IO, value::Union{Int8, Int16, Int32, Int64, Int128})
|
||||
bits = AnnotatedString(bitstring(value))
|
||||
face!(bits[1:1], NUMBER_BIT_FACES.sign)
|
||||
for (; match) in eachmatch(r"0+", bits)
|
||||
if match.offset == 0
|
||||
match = bits[2:match.ncodeunits]
|
||||
end
|
||||
face!(match, :shadow)
|
||||
end
|
||||
if get(io, :compact, false) == true
|
||||
print(io, bits)
|
||||
else
|
||||
signstr = ifelse(value < 0, '-', '+')
|
||||
println(io, "\n ", bits, ifelse(sizeof(value) > 4, "\n", ""),
|
||||
styled" {bold:=} {$(NUMBER_BIT_FACES.sign):$signstr}$value")
|
||||
end
|
||||
end
|
||||
|
||||
memorylayout(io::IO, float::Float64) = floatlayout(io, float, 11)
|
||||
memorylayout(io::IO, float::Float32) = floatlayout(io, float, 8)
|
||||
memorylayout(io::IO, float::Float16) = floatlayout(io, float, 5)
|
||||
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])}"
|
||||
if get(io, :compact, false) == true
|
||||
print(io, hl_bits)
|
||||
else
|
||||
fracbits = 8 * sizeof(float) - expbits - 1
|
||||
fracdp = round(Int, log10(2 ^ (fracbits + 1)))
|
||||
maxexp = 2^(expbits - 1) - 1
|
||||
sign = ifelse(bitstr[1] == '1', '-', '+')
|
||||
bits = reinterpret(UInt64, Float64(float))
|
||||
exponent = Int((bits >> 52) & Base.Ryu.EXP_MASK) - 1023
|
||||
fraction = reinterpret(Float64, bits & Base.Ryu.MANTISSA_MASK | 0x3ff0000000000000)
|
||||
expstr = cpad(if exponent == 1024
|
||||
"Inf"
|
||||
else "2^$exponent" end,
|
||||
expbits - 1, ' ', RoundUp)
|
||||
fracstr = cpad(if exponent == 1024
|
||||
ifelse(fraction == 1.0, "1", "NaN")
|
||||
else
|
||||
Base.Ryu.writefixed(fraction, fracdp + 2)
|
||||
end, fracbits, ' ', RoundUp)
|
||||
hl_info = let eleft = (expbits - 3) ÷ 2
|
||||
eright = (expbits - 3) - eleft
|
||||
fleft = (fracbits - 3) ÷ 2
|
||||
fright = (fracbits - 3) - fleft
|
||||
styled"{$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}"
|
||||
println(io, "\n ", hl_bits, " \n ", hl_info, "\n ", hl_vals,
|
||||
styled"\n {bold:=} ", if -8 < exponent < 8
|
||||
Base.Ryu.writefixed(float, fracdp)
|
||||
else Base.Ryu.writeexp(float, fracdp) end)
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
print(io, cstr, ' ')
|
||||
end
|
||||
if get(io, :compact, false) != true
|
||||
println(io)
|
||||
for chunk in chunks
|
||||
byte = lpad(string(chunk, base=16), 2, '0')
|
||||
print(io, styled" {shadow:└─0x$(byte)─┘}")
|
||||
end
|
||||
println(io)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO char
|
||||
|
||||
# TODO struct
|
Loading…
Reference in New Issue