///|
/// A recursive-descent GraphQL parser producing an executable `Document` AST. It
/// implements the executable half of the GraphQL grammar — operations
/// (query/mutation/subscription and the `{ ... }` shorthand), selection sets,
/// fields with aliases/arguments/directives, variable definitions with default
/// values, named and inline fragments, and directives — matching what
/// `graphql-core` (strawberry's parser) produces. Type-system (SDL) definitions
/// are handled by the code-first schema builder in `schema.mbt`.
///|
/// The parser holds a lexer and a one-token lookahead (`current`).
priv struct Parser {
lexer : Lexer
mut current : Token
}
///|
/// Create a parser over `source`, priming the first token.
fn Parser::new(source : String) -> Parser raise GqlSyntaxError {
let lexer = Lexer::new(source)
let current = lexer.next_token()
{ lexer, current, }
}
///|
/// Consume and return the current token, advancing to the next.
fn Parser::advance(self : Parser) -> Token raise GqlSyntaxError {
let tok = self.current
self.current = self.lexer.next_token()
tok
}
///|
/// Whether the current token has kind `k`.
fn Parser::at(self : Parser, k : TokenKind) -> Bool {
self.current.kind == k
}
///|
/// Whether the current token is a `Name` with text `s` (a keyword check).
fn Parser::at_keyword(self : Parser, s : String) -> Bool {
self.current.kind is Name && self.current.value == s
}
///|
/// A human-readable description of a token kind, for error messages.
fn token_desc(k : TokenKind) -> String {
match k {
Name => "a name"
IntVal => "an int value"
FloatVal => "a float value"
StringVal => "a string value"
BlockStringVal => "a block string"
Bang => "'!'"
Dollar => "'$'"
Amp => "'&'"
ParenL => "'('"
ParenR => "')'"
Spread => "'...'"
Colon => "':'"
Equals => "'='"
At => "'@'"
BracketL => "'['"
BracketR => "']'"
BraceL => "'{'"
BraceR => "'}'"
Pipe => "'|'"
Eof => ""
}
}
///|
/// A short description of the current token, for "found X" diagnostics.
fn Parser::current_desc(self : Parser) -> String {
match self.current.kind {
Name => "name '" + self.current.value + "'"
IntVal | FloatVal => "value '" + self.current.value + "'"
StringVal | BlockStringVal => "a string"
other => token_desc(other)
}
}
///|
/// The current token's position, taken before the token is consumed so a node
/// records where it starts.
fn Parser::pos(self : Parser) -> Pos {
{ line: self.current.line, col: self.current.col, }
}
///|
/// Raise a syntax error at the current token's position (expression position:
/// the polymorphic return lets it stand in for any value).
fn[T] Parser::fail(self : Parser, msg : String) -> T raise GqlSyntaxError {
raise GqlSyntaxError(msg, self.current.line, self.current.col)
}
///|
/// Raise a syntax error at the current token's position (statement position).
fn Parser::err(self : Parser, msg : String) -> Unit raise GqlSyntaxError {
raise GqlSyntaxError(msg, self.current.line, self.current.col)
}
///|
/// Consume a token of kind `k`, or raise "expected ..., found ...".
fn Parser::expect(self : Parser, k : TokenKind) -> Token raise GqlSyntaxError {
if self.at(k) {
self.advance()
} else {
self.fail("expected " + token_desc(k) + ", found " + self.current_desc())
}
}
///|
/// Consume the current `Name` token and return its text, or raise.
fn Parser::expect_name(self : Parser) -> String raise GqlSyntaxError {
self.expect(Name).value
}
///|
/// Consume a `Name` token equal to the keyword `kw`, or raise.
fn Parser::expect_keyword(
self : Parser,
kw : String,
) -> Unit raise GqlSyntaxError {
if self.at_keyword(kw) {
self.advance() |> ignore
} else {
self.fail("expected '" + kw + "', found " + self.current_desc())
}
}
///|
/// Parse a complete document: a sequence of executable definitions up to `Eof`.
fn Parser::parse_document(self : Parser) -> Document raise GqlSyntaxError {
let definitions = []
while !self.at(Eof) {
definitions.push(self.parse_definition())
}
if definitions.length() == 0 {
self.err("expected at least one definition")
}
{ definitions, }
}
///|
/// Parse one top-level definition: an operation (long form or `{ ... }`
/// shorthand), a fragment definition, or a type-system definition the document
/// reader keeps only so that validation can reject it.
fn Parser::parse_definition(self : Parser) -> Definition raise GqlSyntaxError {
if self.at(BraceL) {
return OperationDef(self.parse_operation())
}
if self.at_keyword("query") ||
self.at_keyword("mutation") ||
self.at_keyword("subscription") {
return OperationDef(self.parse_operation())
}
if self.at_keyword("fragment") {
return FragmentDef(self.parse_fragment())
}
// A description belongs to the definition under it, so it is part of where the
// definition starts — and a top-level string can only introduce SDL.
if self.at(StringVal) || self.at(BlockStringVal) {
let pos = self.pos()
ignore(self.advance())
if self.at(Name) && is_type_system_keyword(self.current.value) {
let t = self.parse_type_system_definition()
return TypeSystemDef({
keyword: t.keyword,
kind: t.kind,
name: t.name,
pos,
})
}
self.fail(
"expected a type system definition after a description, found " +
self.current_desc(),
)
}
if self.at(Name) && is_type_system_keyword(self.current.value) {
return TypeSystemDef(self.parse_type_system_definition())
}
self.fail(
"expected 'query', 'mutation', 'subscription', 'fragment' or '{', found " +
self.current_desc(),
)
}
///|
/// Whether `word` opens a type-system definition (§3.1 to §3.9).
fn is_type_system_keyword(word : String) -> Bool {
match word {
"schema"
| "scalar"
| "type"
| "interface"
| "union"
| "enum"
| "input"
| "directive"
| "extend" => true
_ => false
}
}
///|
/// Read a type-system definition's header and discard its body. The body is
/// token-skipped rather than parsed: the definition exists in the document only
/// to be reported, so the reader's job is to find where it ends and get back to
/// the top level.
fn Parser::parse_type_system_definition(
self : Parser,
) -> TypeSystemDefinition raise GqlSyntaxError {
let pos = self.pos()
let keyword = self.advance().value
let mut kind = ""
let mut name = ""
if keyword == "extend" {
if !self.at(Name) {
self.fail("expected a kind after 'extend', found " + self.current_desc())
}
kind = self.advance().value
if self.at(Name) {
name = self.advance().value
}
} else if keyword == "schema" {
// A schema block defines no type, so it has no name to keep.
()
} else if keyword == "directive" {
if self.at(At) {
ignore(self.advance())
}
if self.at(Name) {
name = self.advance().value
}
} else if self.at(Name) {
name = self.advance().value
} else {
self.fail(
"expected a name after '" + keyword + "', found " + self.current_desc(),
)
}
self.skip_type_system_body()
{ keyword, kind, name, pos, }
}
///|
/// Consume tokens up to the end of the current type-system definition: at the
/// top level, another definition's keyword ends it, and a `{` ends it once the
/// definition has had its own block.
fn Parser::skip_type_system_body(self : Parser) -> Unit raise GqlSyntaxError {
let mut depth = 0
let mut seen_block = false
while !self.at(Eof) {
let k = self.current.kind
if depth == 0 {
if k == BraceL {
if seen_block {
return
}
seen_block = true
} else if k == Name && starts_definition(self.current.value) {
return
}
}
match k {
BraceL | ParenL | BracketL => depth = depth + 1
BraceR | ParenR | BracketR => if depth > 0 { depth = depth - 1 }
_ => ()
}
ignore(self.advance())
}
}
///|
/// Whether `word` begins a definition at the top level, so a reader skipping a
/// type-system definition knows it has reached the next one.
fn starts_definition(word : String) -> Bool {
if is_type_system_keyword(word) {
return true
}
match word {
"query" | "mutation" | "subscription" | "fragment" => true
_ => false
}
}
///|
/// Parse an operation definition, including the anonymous query shorthand.
fn Parser::parse_operation(
self : Parser,
) -> OperationDefinition raise GqlSyntaxError {
let pos = self.pos()
if self.at(BraceL) {
let selection_set = self.parse_selection_set()
return {
operation: Query,
name: None,
variable_definitions: [],
directives: [],
selection_set,
pos,
}
}
let operation = match self.current.value {
"query" => Query
"mutation" => Mutation
_ => Subscription
}
self.advance() |> ignore
let name = if self.at(Name) { Some(self.advance().value) } else { None }
let variable_definitions = self.parse_variable_definitions()
let directives = self.parse_directives()
let selection_set = self.parse_selection_set()
{ operation, name, variable_definitions, directives, selection_set, pos, }
}
///|
/// Parse `($v: T = default @dir, ...)`, or an empty list when absent.
fn Parser::parse_variable_definitions(
self : Parser,
) -> Array[VariableDefinition] raise GqlSyntaxError {
let defs = []
if !self.at(ParenL) {
return defs
}
self.advance() |> ignore
while !self.at(ParenR) && !self.at(Eof) {
let pos = self.pos()
self.expect(Dollar) |> ignore
let variable = self.expect_name()
self.expect(Colon) |> ignore
let typ = self.parse_type()
let default_value = if self.at(Equals) {
self.advance() |> ignore
Some(self.parse_value(true))
} else {
None
}
let directives = self.parse_directives()
defs.push({ variable, typ, default_value, directives, pos, })
}
self.expect(ParenR) |> ignore
defs
}
///|
/// Parse a type reference: `Name`, `[Type]`, or either followed by `!`.
fn Parser::parse_type(self : Parser) -> TypeRef raise GqlSyntaxError {
let mut t = if self.at(BracketL) {
self.advance() |> ignore
let inner = self.parse_type()
self.expect(BracketR) |> ignore
ListType(inner)
} else {
NamedType(self.expect_name())
}
if self.at(Bang) {
self.advance() |> ignore
t = NonNullType(t)
}
t
}
///|
/// Parse zero or more `@name(args)` directives.
fn Parser::parse_directives(
self : Parser,
) -> Array[Directive] raise GqlSyntaxError {
let ds = []
while self.at(At) {
let pos = self.pos()
self.advance() |> ignore
let name = self.expect_name()
let arguments = self.parse_arguments(false)
ds.push({ name, arguments, pos, })
}
ds
}
///|
/// Parse `(name: value, ...)`, or an empty list when absent. `const` forbids
/// variables in the values (used for default values and directive args in
/// variable definitions).
fn Parser::parse_arguments(
self : Parser,
const_ : Bool,
) -> Array[Argument] raise GqlSyntaxError {
let args = []
if !self.at(ParenL) {
return args
}
self.advance() |> ignore
while !self.at(ParenR) && !self.at(Eof) {
let pos = self.pos()
let name = self.expect_name()
self.expect(Colon) |> ignore
let value = self.parse_value(const_)
args.push({ name, value, pos, })
}
self.expect(ParenR) |> ignore
args
}
///|
/// Parse a non-empty selection set `{ selection+ }`.
fn Parser::parse_selection_set(
self : Parser,
) -> Array[Selection] raise GqlSyntaxError {
self.expect(BraceL) |> ignore
let sels = []
while !self.at(BraceR) && !self.at(Eof) {
sels.push(self.parse_selection())
}
self.expect(BraceR) |> ignore
if sels.length() == 0 {
self.err("selection set must contain at least one selection")
}
sels
}
///|
/// Parse one selection: a field, a `...Name` fragment spread, or a
/// `... on Type { ... }` / `... { ... }` inline fragment.
fn Parser::parse_selection(self : Parser) -> Selection raise GqlSyntaxError {
if self.at(Spread) {
let pos = self.pos()
self.advance() |> ignore
if self.at_keyword("on") {
self.advance() |> ignore
let cond = self.expect_name()
let directives = self.parse_directives()
let sels = self.parse_selection_set()
return InlineFragmentSel(Some(cond), directives, sels, pos)
}
if self.at(Name) {
let name = self.advance().value
let directives = self.parse_directives()
return FragmentSpreadSel(name, directives, pos)
}
let directives = self.parse_directives()
let sels = self.parse_selection_set()
return InlineFragmentSel(None, directives, sels, pos)
}
FieldSel(self.parse_field())
}
///|
/// Parse a field: `alias: name(args) @dir { ... }` (alias and the trailing
/// selection set are optional).
fn Parser::parse_field(self : Parser) -> QueryField raise GqlSyntaxError {
let pos = self.pos()
let first = self.expect_name()
let mut field_alias : String? = None
let mut name = first
if self.at(Colon) {
self.advance() |> ignore
field_alias = Some(first)
name = self.expect_name()
}
let arguments = self.parse_arguments(false)
let directives = self.parse_directives()
let selection_set = if self.at(BraceL) {
self.parse_selection_set()
} else {
[]
}
{ alias_: field_alias, name, arguments, directives, selection_set, pos, }
}
///|
/// Parse a fragment definition: `fragment Name on Type @dir { ... }`. The
/// fragment name must not be the reserved word `on`.
fn Parser::parse_fragment(
self : Parser,
) -> FragmentDefinition raise GqlSyntaxError {
let pos = self.pos()
self.expect_keyword("fragment")
if self.at_keyword("on") {
self.err("fragment name must not be 'on'")
}
let name = self.expect_name()
self.expect_keyword("on")
let type_condition = self.expect_name()
let directives = self.parse_directives()
let selection_set = self.parse_selection_set()
{ name, type_condition, directives, selection_set, pos, }
}
///|
/// Parse an input value. `const` (used for default values) forbids `$variable`.
fn Parser::parse_value(
self : Parser,
const_ : Bool,
) -> Value raise GqlSyntaxError {
match self.current.kind {
Dollar => {
if const_ {
self.err("variable is not allowed in a constant value")
}
self.advance() |> ignore
Variable(self.expect_name())
}
IntVal => IntValue(self.advance().value)
FloatVal => FloatValue(self.advance().value)
StringVal => StringValue(self.advance().value, false)
BlockStringVal => StringValue(self.advance().value, true)
Name =>
match self.current.value {
"true" => {
self.advance() |> ignore
BooleanValue(true)
}
"false" => {
self.advance() |> ignore
BooleanValue(false)
}
"null" => {
self.advance() |> ignore
NullValue
}
_ => EnumValue(self.advance().value)
}
BracketL => {
self.advance() |> ignore
let items = []
while !self.at(BracketR) && !self.at(Eof) {
items.push(self.parse_value(const_))
}
self.expect(BracketR) |> ignore
ListValue(items)
}
BraceL => {
self.advance() |> ignore
let fields = []
while !self.at(BraceR) && !self.at(Eof) {
let k = self.expect_name()
self.expect(Colon) |> ignore
let v = self.parse_value(const_)
fields.push((k, v))
}
self.expect(BraceR) |> ignore
ObjectValue(fields)
}
_ => self.fail("expected a value, found " + self.current_desc())
}
}
///|
/// Parse a GraphQL executable document from source text. This is the front half
/// of the executor: `parse(query).definitions` yields the operations and
/// fragments to validate and execute.
pub fn parse(source : String) -> Document raise GqlSyntaxError {
let p = Parser::new(source)
p.parse_document()
}