///|
priv struct JcParser {
tokens : Array[Token]
options : ParseOptions
diagnostics : Array[Diagnostic]
mut cursor : Int
mut nodes : Int
}
///|
fn JcParser::issue(
self : JcParser,
code : String,
token : Token,
message : String,
) -> Unit {
if self.diagnostics.length() < self.options.max_errors {
self.diagnostics.push({
code,
offset: token.offset,
length: token.length,
message,
})
}
}
///|
fn JcParser::peek(self : JcParser) -> Token {
self.tokens[self.cursor]
}
///|
fn JcParser::advance(self : JcParser) -> Token {
let token = self.peek()
if token.kind != Eof {
self.cursor += 1
}
token
}
///|
fn jc_leaf(kind : NodeKind, token : Token, value : Json) -> Node {
{ kind, offset: token.offset, length: token.length, value, children: [], }
}
///|
fn JcParser::skip_container(self : JcParser) -> Int {
let mut depth = 0
let mut finish = self.peek().offset
while self.peek().kind != Eof {
let token = self.advance()
finish = token.offset + token.length
match token.kind {
OpenBrace | OpenBracket => depth += 1
CloseBrace | CloseBracket => {
depth -= 1
if depth <= 0 {
break
}
}
_ => ()
}
}
finish
}
///|
fn JcParser::value(self : JcParser, depth : Int) -> Node? {
let token = self.peek()
self.nodes += 1
if self.nodes > 100000 {
self.issue("NodeLimitExceeded", token, "syntax tree exceeds 100000 values")
while self.peek().kind != Eof {
self.advance() |> ignore
}
return None
}
match token.kind {
OpenBrace | OpenBracket => {
if depth >= self.options.max_depth {
self.issue(
"NestingTooDeep", token, "container nesting exceeds the configured limit",
)
let finish = self.skip_container()
return Some({
kind: if token.kind == OpenBrace {
ObjectNode
} else {
ArrayNode
},
offset: token.offset,
length: finish - token.offset,
value: Json::null(),
children: [],
})
}
Some(self.container(depth + 1, token.kind == OpenBrace))
}
StringToken => {
self.advance() |> ignore
Some(jc_leaf(StringNode, token, token.value.to_json()))
}
NumberToken => {
self.advance() |> ignore
let value = if token.error is Some(_) {
Json::null()
} else {
@json.parse(token.value) catch {
// Number grammar was checked by the lexer. Preserve a valid spelling
// even beyond Double range; source operations never use this estimate.
_ =>
Json::number(
if token.value.has_prefix("-") {
-1.0 / 0.0
} else {
1.0 / 0.0
},
repr=token.value,
)
}
}
let value = match value {
Number(estimate, ..) => Json::number(estimate, repr=token.value)
_ => value
}
Some(jc_leaf(NumberNode, token, value))
}
TrueToken | FalseToken => {
self.advance() |> ignore
Some(jc_leaf(BooleanNode, token, (token.kind == TrueToken).to_json()))
}
NullToken => {
self.advance() |> ignore
Some(jc_leaf(NullNode, token, Json::null()))
}
_ => {
self.issue("ValueExpected", token, "expected a JSON value")
None
}
}
}
///|
fn JcParser::container(self : JcParser, depth : Int, object : Bool) -> Node {
let opening = self.advance()
let closing = if object { CloseBrace } else { CloseBracket }
let children : Array[Node] = []
let members : Map[String, Json] = Map([])
let elements : Array[Json] = []
while self.peek().kind != Eof &&
self.peek().kind != CloseBrace &&
self.peek().kind != CloseBracket {
if self.peek().kind == Comma {
let comma = self.advance()
self.issue(
if object {
"PropertyNameExpected"
} else {
"ValueExpected"
},
comma,
"unexpected comma",
)
continue
}
let before = self.cursor
if object {
if self.peek().kind != StringToken {
self.issue(
"PropertyNameExpected",
self.peek(),
"object keys must be quoted strings",
)
self.advance() |> ignore
continue
}
let key_token = self.advance()
let key = jc_leaf(StringNode, key_token, key_token.value.to_json())
if self.peek().kind == Colon {
self.advance() |> ignore
} else {
self.issue(
"ColonExpected",
self.peek(),
"expected ':' after an object key",
)
}
let value = match self.value(depth) {
Some(node) => node
None =>
{
kind: NullNode,
offset: self.peek().offset,
length: 0,
value: Json::null(),
children: [],
}
}
if members.contains(key_token.value) {
self.issue(
"DuplicateKey", key_token, "duplicate object key; edits require unambiguous paths",
)
}
members[key_token.value] = value.value
children.push({
kind: PropertyNode,
offset: key.offset,
length: (value.offset + value.length - key.offset).max(key.length),
value: value.value,
children: [key, value],
})
} else {
match self.value(depth) {
Some(node) => {
children.push(node)
elements.push(node.value)
}
None => if self.peek().kind != Eof { self.advance() |> ignore }
}
}
if self.cursor == before {
self.advance() |> ignore
}
if self.peek().kind == Comma {
let comma = self.advance()
if self.peek().kind == closing && !self.options.allow_trailing_comma {
self.issue("TrailingComma", comma, "trailing commas are disabled")
}
} else if self.peek().kind != closing &&
self.peek().kind != Eof &&
self.peek().kind != CloseBrace &&
self.peek().kind != CloseBracket {
self.issue("CommaExpected", self.peek(), "expected ',' between values")
}
}
let finish = if self.peek().kind == closing {
let end = self.advance()
end.offset + end.length
} else {
self.issue(
if object {
"CloseBraceExpected"
} else {
"CloseBracketExpected"
},
self.peek(),
"container has no matching closing delimiter",
)
self.peek().offset
}
{
kind: if object {
ObjectNode
} else {
ArrayNode
},
offset: opening.offset,
length: finish - opening.offset,
value: if object {
members.to_json()
} else {
elements.to_json()
},
children,
}
}
///|
/// Parse a tolerant, source-positioned JSONC tree. Diagnostics make partial trees
/// unsuitable for mutation. Duplicate keys are retained, diagnosed, and project
/// to last-key-wins Json. Depth is 1..512, diagnostics 1..1000; values <=100000.
pub fn parse(
text : String,
options? : ParseOptions = ParseOptions::default(),
) -> Document {
let tokens = scan(text)
let diagnostics : Array[Diagnostic] = []
if options.max_depth < 1 ||
options.max_depth > 512 ||
options.max_errors < 1 ||
options.max_errors > 1000 {
diagnostics.push({
code: "InvalidOptions",
offset: 0,
length: 0,
message: "max_depth must be 1..512 and max_errors 1..1000",
})
return { text, tokens, root: None, diagnostics, }
}
let significant : Array[Token] = []
let parser : JcParser = {
tokens: significant,
options,
diagnostics,
cursor: 0,
nodes: 0,
}
for token in tokens {
if token.error is Some(code) {
parser.issue(code, token, "invalid JSONC token")
}
if token.kind == LineComment || token.kind == BlockComment {
if !options.allow_comments {
parser.issue("InvalidCommentToken", token, "comments are disabled")
}
}
// Invalid symbols already have lexical diagnostics. Skipping them here
// permits useful recovery of the following value instead of discarding an
// otherwise valid subtree after a typo before the root or a member value.
if !jc_trivia(token.kind) && token.kind != Unknown {
significant.push(token)
}
}
let root = if parser.peek().kind == Eof && options.allow_empty {
None
} else {
parser.value(0)
}
if root is None && parser.peek().kind != Eof {
parser.advance() |> ignore
}
while parser.peek().kind != Eof {
parser.issue(
"EndOfFileExpected",
parser.advance(),
"unexpected content after the root value",
)
}
{ text, tokens, root, diagnostics, }
}