Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions Manifest-v1.12.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 26 additions & 2 deletions src/JSON.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ end

@enum Error InvalidJSON UnexpectedEOF ExpectedOpeningObjectChar ExpectedOpeningQuoteChar ExpectedOpeningArrayChar ExpectedClosingArrayChar ExpectedComma ExpectedColon ExpectedNewline InvalidChar InvalidNumber InvalidUTF16

@noinline function invalid(error, buf, pos::Int, T)
@noinline function invalid(error, buf, pos::Int, @nospecialize(T))
# compute which line the error falls on by counting “\n” bytes up to pos
cus = buf isa AbstractString ? codeunits(buf) : buf
line_no = count(b -> b == UInt8('\n'), view(cus, 1:pos)) + 1
Expand All @@ -48,8 +48,13 @@ end
snippet = replace(snippet, r"[\b\f\n\r\t]" => " ")
# we call @invoke here to avoid --trim verify errors
caret = @invoke(repeat(" "::String, (erri + 2)::Integer)) * "^"
# the type renders via an isa ladder with per-branch typeasserts:
# interpolating the type object infers show machinery per target type (a
# large per-type TTFX tax) and is dynamic under --trim now that this
# helper is @nospecialize
tname = _typenamestr(T)
msg = """
invalid JSON at byte position $(pos) (line $line_no) parsing type $T: $error
invalid JSON at byte position $(pos) (line $line_no) parsing type $(tname): $error
$snippet$(error == UnexpectedEOF ? " <EOF>" : "...")
$caret
"""
Expand Down Expand Up @@ -87,6 +92,7 @@ end

include("lazy.jl")
include("parse.jl")
include("tier0.jl")
include("write.jl")

"""
Expand Down Expand Up @@ -125,10 +131,28 @@ print(a, indent=nothing) = print(stdout, a, indent)
"See [`json`](@ref)."
print

function __init__()
# :hot-annotated struct definitions in downstream packages fire this hook
# during their precompilation, compiling the typed parse/write paths for
# each annotated type into that package's image
StructUtils.register_hot_hook!(_hot_json_hook)
return nothing
end

# exercises the untyped engine plus the tier-0 typed default (engine → field
# table interpreter), so a downstream user's first typed parse of any
# eligible struct costs a table build instead of compiling a descent
@kwarg struct _EngineWorkload
a::Int = 0
b::Union{String,Nothing} = nothing
c::Vector{Float64} = Float64[]
end

@compile_workload begin
x = JSON.parse("{\"a\": 1, \"b\": null, \"c\": true, \"d\": false, \"e\": \"\", \"f\": [1,null,true], \"g\": {\"key\": \"value\"}}")
json = JSON.json(x)
isvalidjson(json)
JSON.parse("{\"a\": 1, \"b\": \"x\", \"c\": [1.5], \"z\": null}", _EngineWorkload)
end


Expand Down
70 changes: 64 additions & 6 deletions src/parse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ import StructUtils: StructStyle

abstract type JSONStyle <: StructStyle end

# lazy values carry their own lift/descent protocol; the tier-0 interpreter
# must never drive them directly (the fused engine in tier0.jl is the
# interpreter-shaped path for lazy input)
StructUtils.interpsource(::LazyValues) = false

# defining a custom style allows us to pass a non-default dicttype `O` through JSON.parse,
# while still delegating custom behavior to an inner StructStyle if one was provided
struct JSONReadStyle{O,T,S} <: JSONStyle
Expand Down Expand Up @@ -184,19 +189,41 @@ StructUtils.structlike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONSt
StructUtils.structlike(st::JSONReadStyle{O,N,S}, ::Type{T}) where {O,N,S<:JSONStyle,T<:NamedTuple} =
StructUtils.structlike(st.style, T)

function jsonreadstyle(::Type{T}, ::Type{O}, null, style::StructStyle, unknown_fields::Symbol) where {T,O}
function jsonreadstyle(@nospecialize(T), ::Type{O}, null, style::StructStyle, unknown_fields::Symbol) where {O}
ignore_unknown_fields =
unknown_fields === :ignore ? true :
unknown_fields === :error ? false :
throw(ArgumentError("`unknown_fields` must be `:ignore` or `:error`, got `$(repr(unknown_fields))`"))
throw(ArgumentError(string("`unknown_fields` must be `:ignore` or `:error`, got `:", String(unknown_fields), "`")))
if T === Any && !ignore_unknown_fields
throw(ArgumentError("`unknown_fields` is only supported when parsing into a target type or existing object"))
end
return JSONReadStyle{O}(null, style, ignore_unknown_fields)
end

@noinline unknownfielderror(::Type{T}, key) where {T} =
ArgumentError("encountered unknown JSON member $(repr(key)) while parsing `$T`")
# isa-laddered string extraction: no per-target-type inference of show
# machinery (the 1.5.1 TTFX regression class), and no dynamic repr-of-Any
# for the trim verifier to reject
@noinline function unknownfielderror(@nospecialize(T), @nospecialize(key))
# per-branch typeasserts: narrowing on @nospecialize arguments doesn't
# stick, and the verifier needs each call fully concrete
keystr = key isa String ? (key::String) :
key isa PtrString ? convert(String, key::PtrString) :
key isa Symbol ? String(key::Symbol) :
key isa Int ? string(key::Int) : "<key>"
return ArgumentError(string("encountered unknown JSON member \"", keystr::String,
"\" while parsing `", _typenamestr(T), "`"))
end

function _typenamestr(@nospecialize(T))
# unwrap UnionAlls by hand: Base's nameof(::UnionAll) recurses through an
# Any-typed body, which the trim verifier can't resolve
body = T
while body isa UnionAll
body = getfield(body::UnionAll, :body)
end
body isa DataType && return String(nameof(body::DataType)::Symbol)
return "<type>"
end

function StructUtils.unknownfield(st::JSONReadStyle, ::Type{T}, key, value) where {T}
st.ignore_unknown_fields || throw(unknownfielderror(T, key))
Expand Down Expand Up @@ -232,12 +259,37 @@ parse(x::LazyValue, ::Type{T}=Any;
unknown_fields::Symbol=:ignore) where {T,O} =
@inline _parse(x, T, dicttype, null, jsonreadstyle(T, O, null, style, unknown_fields))


function _parse(x::LazyValue, ::Type{T}, dicttype::Type{O}, null, style::StructStyle) where {T,O}
if StructUtils.TRIM_BUILD
# trim binaries need the static call graph
y, pos = StructUtils.make(style, T, x)
getisroot(x) && checkendpos(x, T, pos)
return y
end
if !StructUtils.ishot(T) && style isa _FUSED_STYLE
# the default: one lazy pass drives the field-table interpreter —
# any target type, and a new type costs a table build, not a compile
y, pos = _fused_make(style, T, x)
getisroot(x) && checkendpos(x, T, pos)
return y::T
end
# invokelatest keeps the per-type descent out of this function's
# compilation: a plain call here would specialize the whole descent for
# every parsed type, including the ones the engine above handles. Types
# that route here (`:hot`, custom styles/dicttype/null) pay one dynamic
# dispatch per parse.
return Base.invokelatest(_parse_specialized, x, T, style)
end

function _parse_specialized(x::LazyValue, ::Type{T}, style::StructStyle) where {T}
y, pos = StructUtils.make(style, T, x)
getisroot(x) && checkendpos(x, T, pos)
return y
end



mutable struct ValueClosure
value::Any
ValueClosure() = new()
Expand All @@ -260,7 +312,7 @@ parse!(x::LazyValue, obj::T;
# for LazyValue, if x started at the beginning of the JSON input,
# then we want to ensure that the entire input was consumed
# and error if there are any trailing invalid JSON characters
function checkendpos(x::LazyValue, ::Type{T}, pos::Int) where {T}
function checkendpos(x::LazyValue, @nospecialize(T), pos::Int)
buf = getbuf(x)
len = getlength(buf)
if pos <= len
Expand Down Expand Up @@ -372,7 +424,10 @@ function StructUtils.make(st::StructStyle, ::Type{Any}, x::LazyValues)
elseif type == JSONTypes.TRUE || type == JSONTypes.FALSE
return StructUtils.lift(st, Bool, x)
else
throw(ArgumentError("cannot parse $x"))
# no value interpolation: `show(::LazyValue)` materializes and prints
# a LazyArray through Base's matrix-show machinery — a large dynamic
# fan-out under --trim; `invalid` renders position + type statically
invalid(InvalidJSON, getbuf(x), getpos(x), Any)
end
end

Expand Down Expand Up @@ -502,6 +557,9 @@ function StructUtils.lift(style::JSONReadStyle, ::Type{T}, x::LazyValues, tags=(
end
end

# JSONText fields must reach this make method rather than being field-parsed
StructUtils.custommake(::Type{JSONText}) = true

function StructUtils.make(::StructStyle, ::Type{JSONText}, x::LazyValues)
buf = getbuf(x)
pos = getpos(x)
Expand Down
Loading
Loading