///|
/// Compile-time reference context. The arena arrays (`paths`, `targets`,
/// `visited`, `doc_of`, `resource_of`, `nodes`) are indexed by slot; every
/// slot belongs to one source document (`Doc`) and one schema resource.
/// `registry` maps canonical resource identity URIs to resource indexes;
/// `sites` records where each resource lives (document + root pointer).
/// Resolution descends the indexed documents directly: compilation stays
/// I/O-free and unknown external URIs fail compilation, never fetch.
priv struct RefIndex {
docs : Array[Doc]
paths : Array[String]
targets : Array[Json]
visited : Array[Bool]
doc_of : Array[Int32]
resource_of : Array[Int32]
mut reference_depth : Int32
nodes : Array[Plan]
resources : Array[ResourceMeta]
sites : Array[(Int32, String)]
registry : Map[String, Int32]
anchors : Map[Int32, Map[String, Int32]]
mut collect_evaluated : Bool
mut track_dynamic_scope : Bool
dialects : Map[String, Dialect]
resolving_dialects : Set[String]
}
///|
/// Compile an arena slot if no walk has reached it yet. The visited flag is
/// raised on entry to `compile_into`, so it doubles as the active-chain mark
/// and mutually recursive unknown-keyword targets terminate: the frame that
/// owns the slot writes its plan on return.
fn ensure_compiled(
index : RefIndex,
slot : Int32,
limit : Int32,
) -> Unit raise CompileError {
if !index.visited[slot] {
if index.reference_depth >= limit {
raise CompileError::DepthLimit(limit)
}
index.reference_depth += 1
defer {
index.reference_depth -= 1
}
ignore(
compile_into(
index,
index.doc_of[slot],
index.resource_of[slot],
index.targets[slot],
index.paths[slot],
0,
limit,
),
)
}
}
///|
fn slot_at(
index : RefIndex,
doc : Int32,
pointer : String,
path : String,
) -> Int32 raise CompileError {
match index.docs[doc].slots.get(pointer) {
Some(slot) => slot
// Unreachable for positions the resource scan has seen; reachable when a
// pointer fragment addresses a non-schema registry document root.
None => {
invalid_keyword(path, "reference does not resolve to a schema")
0
}
}
}
///|
/// Split a reference into the part before `#` and the raw fragment. An empty
/// prefix keeps resolution inside the current resource; a nonempty prefix
/// names a resource by URI and must be registered.
fn resolve_reference(
index : RefIndex,
resource : Int32,
reference : String,
path : String,
limit : Int32,
) -> (Int32, String?) raise CompileError {
validate_uri_reference(reference, path)
let (before, fragment) = split_at_first(reference, '#')
if before == "" {
(resource, fragment)
} else {
let identity = canonical_uri(index.resources[resource].uri, before)
(load_resource(index, identity, path, limit), fragment)
}
}
///|
/// The slot of the schema a resource's root position occupies. Missing only
/// for registry entries whose document is not a schema at all.
fn resource_root_slot(
index : RefIndex,
doc : Int32,
root_pointer : String,
path : String,
) -> Int32 raise CompileError {
match index.docs[doc].slots.get(root_pointer) {
Some(slot) => slot
None => {
invalid_keyword(path, "reference target is not a schema")
0
}
}
}
///|
fn static_anchor_slot(
index : RefIndex,
resource : Int32,
name : String,
) -> Int32? {
let ordinary = match index.anchors.get(resource) {
Some(anchors) => anchors.get(name)
None => None
}
match ordinary {
Some(slot) => Some(slot)
None => index.resources[resource].dynamic_anchors.get(name)
}
}
///|
/// Resolve a `$ref` to its arena slot. The reference resolves against the
/// canonical URI of the current schema resource (RFC 3986): an empty fragment
/// is the resource root, a `/`-led fragment is a JSON Pointer relative to
/// that root, and a plain-name fragment is an `$anchor` of the resource.
fn resolve_ref(
index : RefIndex,
resource : Int32,
reference : String,
path : String,
limit : Int32,
) -> Int32 raise CompileError {
let (target, fragment) = resolve_reference(
index, resource, reference, path, limit,
)
let (doc, root_pointer) = index.sites[target]
match fragment {
None | Some("") => {
let slot = resource_root_slot(index, doc, root_pointer, path)
ensure_compiled(index, slot, limit)
slot
}
Some(raw) => {
let decoded = percent_decode(raw, path)
let slot = if decoded.get_char(0) == Some('/') {
descend_pointer(index, doc, root_pointer, decoded, path)
} else {
let name = decoded
match static_anchor_slot(index, target, name) {
Some(slot) => slot
None => {
invalid_keyword(
path,
"reference does not resolve: unknown anchor \"" + name + "\"",
)
0
}
}
}
ensure_compiled(index, slot, limit)
slot
}
}
}
///|
/// Resolve a `$dynamicRef` to its initial target and dynamic name. Per
/// 2020-12 core section 8.2.3.2, runtime resolution participates only when
/// the initially resolved target carries a `$dynamicAnchor` with the
/// fragment's name; otherwise the reference behaves exactly like `$ref` —
/// including pointer fragments, empty fragments and static `$anchor` targets.
/// The evaluator then replaces the initial target with the outermost
/// resource of the dynamic scope defining the same `$dynamicAnchor`.
fn resolve_dynamic_ref(
index : RefIndex,
resource : Int32,
reference : String,
path : String,
limit : Int32,
) -> (Int32, String?) raise CompileError {
let (target, fragment) = resolve_reference(
index, resource, reference, path, limit,
)
let (doc, root_pointer) = index.sites[target]
match fragment {
None | Some("") => {
let slot = resource_root_slot(index, doc, root_pointer, path)
ensure_compiled(index, slot, limit)
(slot, None)
}
Some(raw) => {
let decoded = percent_decode(raw, path)
if decoded.get_char(0) == Some('/') {
let slot = descend_pointer(index, doc, root_pointer, decoded, path)
ensure_compiled(index, slot, limit)
(slot, None)
} else {
let name = decoded
match index.resources[target].dynamic_anchors.get(name) {
Some(slot) => {
ensure_compiled(index, slot, limit)
(slot, Some(name))
}
None =>
match static_anchor_slot(index, target, name) {
Some(slot) => {
ensure_compiled(index, slot, limit)
(slot, None)
}
None => {
invalid_keyword(
path,
"dynamic reference does not resolve: no \"$dynamicAnchor\" named \"" +
name +
"\"",
)
(0, None)
}
}
}
}
}
}
}
///|
/// Walk a percent-decoded JSON Pointer from a document root through the
/// resource's embedded position, building the canonical pointer used for
/// slot lookup. The resource root pointer is already canonical (escaped), so
/// its segments only need unescaping to step through values; fragment
/// segments are decoded first, then re-escaped canonically.
fn descend_pointer(
index : RefIndex,
doc : Int32,
root_pointer : String,
decoded : String,
path : String,
) -> Int32 raise CompileError {
let mut current = index.docs[doc].root
for segment in root_pointer.split("/")[1:] {
current = pointer_child(current, pointer_unescape(segment, path), path)
}
let mut canonical = root_pointer
for segment in decoded.split("/")[1:] {
let key = pointer_unescape(segment, path)
current = pointer_child(current, key, path)
canonical = pointer_step(canonical, key)
}
guard current is (Object(_) | True | False) else {
invalid_keyword(path, "reference target is not a schema")
return 0
}
slot_at(index, doc, canonical, path)
}
///|
fn pointer_child(
current : Json,
key : String,
path : String,
) -> Json raise CompileError {
match current {
Object(fields) =>
match fields.get(key) {
Some(child) => child
None => {
invalid_keyword(path, "reference does not resolve: missing member")
Json::null()
}
}
Array(values) =>
match array_token(key) {
Some(position) =>
if position < values.length() {
values[position]
} else {
invalid_keyword(
path, "reference does not resolve: array index out of range",
)
Json::null()
}
None => {
invalid_keyword(
path, "reference does not resolve: invalid array index token",
)
Json::null()
}
}
_ => {
invalid_keyword(path, "reference does not resolve through a scalar")
Json::null()
}
}
}
///|
/// RFC 3986 percent-decoding over UTF-8 bytes. Decoding happens before JSON
/// Pointer unescaping (URI layer first). Malformed escapes and non-UTF-8
/// byte sequences fail compilation instead of silently mangling a pointer.
fn percent_decode(text : String, path : String) -> String raise CompileError {
let raw = @utf8.encode(text)
let bytes : Array[Byte] = []
let length = raw.length()
let mut cursor = 0
while cursor < length {
if raw[cursor] is b'%' {
if cursor + 2 >= length {
invalid_keyword(path, "truncated percent-escape in reference")
return ""
}
let high = hex_digit(raw[cursor + 1])
let low = hex_digit(raw[cursor + 2])
if high is None || low is None {
invalid_keyword(path, "invalid percent-escape in reference")
return ""
}
bytes.push((high.unwrap() * 16 + low.unwrap()).to_byte())
cursor += 3
} else {
bytes.push(raw[cursor])
cursor += 1
}
}
@utf8.decode(Bytes::from_array(bytes[:])) catch {
_ => {
invalid_keyword(path, "reference fragment is not valid UTF-8")
""
}
}
}
///|
fn hex_digit(unit : Byte) -> Int32? {
let value = unit.to_int()
let digit = value - '0'.to_int()
if digit >= 0 && digit <= 9 {
Some(digit)
} else if value >= 'a'.to_int() && value <= 'f'.to_int() {
Some(value - 'a'.to_int() + 10)
} else if value >= 'A'.to_int() && value <= 'F'.to_int() {
Some(value - 'A'.to_int() + 10)
} else {
None
}
}
///|
/// JSON Pointer unescaping: `~1` becomes `/` and `~0` becomes `~`. A `~`
/// followed by any other unit, or a trailing `~`, is a malformed pointer.
fn pointer_unescape(
segment : StringView,
path : String,
) -> String raise CompileError {
let builder = StringBuilder()
let mut escaped = false
for ch in segment {
if escaped {
match ch {
'0' => {
builder.write_char('~')
escaped = false
}
'1' => {
builder.write_char('/')
escaped = false
}
_ => {
invalid_keyword(path, "invalid JSON Pointer escape in reference")
return ""
}
}
} else if ch is '~' {
escaped = true
} else {
builder.write_char(ch)
}
}
if escaped {
invalid_keyword(path, "invalid JSON Pointer escape in reference")
return ""
}
builder.to_string()
}
///|
/// RFC 6901 array tokens: `"0"` or a canonical decimal without leading zeros.
/// `-` and non-canonical forms never match, so they resolve as dangling.
fn array_token(segment : String) -> Int32? {
let length = segment.length()
if length <= 0 {
return None
}
let first = segment[0].to_int() - '0'.to_int()
if first < 0 || first > 9 {
return None
}
if length == 1 {
return Some(first)
}
if first == 0 {
return None
}
let mut value = first
for position in 1.. 9 {
return None
}
if value > 214748364 || (value == 214748364 && digit > 7) {
return None
}
value = value * 10 + digit
}
Some(value)
}