//===----------------------------------------------------------------------===// // Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// /// Utilities for managing Pkl source code. @ModuleInfo { minPklVersion = "0.33.0" } @Since { version = "0.33.0" } module pkl.syntax /// Utilities to parse Pkl source code to a syntax tree. class Parser { /// Parse the string or resource as a Pkl module, returning either a syntax node or /// throwing an error. external function parseModule(source: String | Resource): Node /// Parse the string or resource as a Pkl module, returning either a syntax node or null /// in case of an error. external function parseModuleOrNull(source: String | Resource): Node? /// Parse the string or resource as a Pkl expression, returning either a syntax node or /// throwing an error. external function parseExpression(source: String | Resource): Node /// Parse the string or resource as a Pkl expression, returning either a syntax node or null /// in case of an error. external function parseExpressionOrNull(source: String | Resource): Node? } /// Renders a [Node] back to Pkl source code, using the canonical Pkl formatter. class FormattingRenderer { /// The grammar version to target. /// /// `"V1"` matches Pkl 0.25 - 0.29, `"V2"` matches Pkl 0.30+. grammarVersion: "V1" | "V2" = "V2" /// Render [node] as Pkl source code. external function render(node: Node): String } /// A generic, untyped node in the Pkl syntax tree. /// /// A node is either a *leaf*, carrying no [children], or a *branch*, whose content is its /// [children]. Either way, [text] is the source code the node spans. class Node { /// The kind of this node. type: NodeType /// The child nodes, in source order. Empty for leaf nodes. children: List(isTerminal.implies(isEmpty)) /// The parent node, or [null] for the root and for nodes not attached to a tree. hidden parent: Node? /// The verbatim source text for this node. /// /// [FormattingRenderer] only renders this text if this is a [terminal][isTerminal] node. text: String?(isTerminal.implies(this != null)) /// The source location of this node. span: Span? /// Tells if this is a terminal node; i.e. a node that spans a single token of the grammar. /// /// A terminal never has [children], but not every node without [children] is a terminal. fixed isTerminal: Boolean = type is TerminalNodeType /// Fold [operator] over this node and its descendants, top-down in pre-order. /// /// Example: /// ``` /// // count the if-expressions in a module /// module.fold(0, (acc, n) -> if (n.type == "if_expr") acc + 1 else acc) /// ``` external function fold(initial: Result, operator: (Result, Node) -> Result): Result /// Returns the first child matching [predicate], searching depth-first in pre-order. /// /// *Child* means any node below this one, at any depth. /// This node itself is not considered. /// The search stops at the first match. /// /// Throws if no child matches [predicate]. /// /// Example: /// ``` /// // the name of the first class declared in a module /// module.findChild((n) -> n.type == "class").findChildOfType("identifier").text /// ``` external function findChild(predicate: (Node) -> Boolean): Node /// Same as [findChild()] but returns [null] if no child matches [predicate]. external function findChildOrNull(predicate: (Node) -> Boolean): Node? /// Returns every child matching [predicate], in depth-first pre-order. /// /// *Child* means any node below this one, at any depth. /// This node itself is not considered. /// A matching node is searched for further matches, so a match may contain another. external function findChildren(predicate: (Node) -> Boolean): List /// Returns the first child whose [type] is [type]. /// /// Throws if no child has this type. external function findChildOfType(type: NodeType): Node /// Same as [findChildOfType()] but returns [null] if no child has this type. external function findChildOfTypeOrNull(type: NodeType): Node? /// Returns every child whose [type] is [type], in depth-first pre-order. external function findChildrenOfType(type: NodeType): List /// Returns this node with the first child matching [predicate] replaced by [replacer]'s result. /// /// The child replaced is the one [findChild()] returns. If there is none, the tree is /// returned unchanged. /// The replacement is left as it is, so it is not searched for further matches. external function replaceChild(predicate: (Node) -> Boolean, replacer: (Node) -> Node): Node /// Returns this node with every child matching [predicate] replaced by [replacer]'s result. /// /// The children replaced are those [findChildren()] returns, except that a replacement is left /// as it is, so a match nested in another match is left to [replacer]. external function replaceChildren(predicate: (Node) -> Boolean, replacer: (Node) -> Node): Node /// Returns this node with the first child whose [type] is [type] replaced by [replacer]'s result. /// /// If there is no such child, the tree is returned unchanged. external function replaceChildOfType(type: NodeType, replacer: (Node) -> Node): Node /// Returns this node with every child whose [type] is [type] replaced by [replacer]'s result. external function replaceChildrenOfType(type: NodeType, replacer: (Node) -> Node): Node /// Returns the nearest parent matching [predicate]. /// /// Throws if no parent matches [predicate]. external function findParent(predicate: (Node) -> Boolean): Node /// Same as [findParent()] but returns [null] if no parent matches [predicate]. external function findParentOrNull(predicate: (Node) -> Boolean): Node? /// Returns the nearest parent whose [type] is [type]. /// /// Throws if no parent has this type. external function findParentOfType(type: NodeType): Node /// Same as [findParentOfType()] but returns [null] if no parent has this type. external function findParentOfTypeOrNull(type: NodeType): Node? /// Returns every parent matching [predicate], nearest first. external function findParents(predicate: (Node) -> Boolean): List /// Tells whether any parent matches [predicate]. external function hasParent(predicate: (Node) -> Boolean): Boolean /// Tells whether any parent's [type] is [type]. external function hasParentOfType(type: NodeType): Boolean } /// A range of source code, spanning [start] (inclusive) to [end] (exclusive). class Span { /// The start of this span. start: SourceLocation /// The end of this span. end: SourceLocation /// The display URI of this span. /// /// Rendered for the editor configured in the Pkl settings file, the same way a location is /// rendered in a stack trace. displayUri: String } /// A position in source code, given as a 1-based line and column. class SourceLocation { /// The 1-based line number of this source location. line: UInt /// The 1-based column number of this source location. column: UInt /// A URI identifying this source location, suitable for display. /// /// The exact format depends on where this location came from and on the editor configured in the /// Pkl settings file. displayUri: String } /// The kinds of node in the Pkl syntax tree. typealias NodeType = TerminalNodeType | ExprNodeType | TypeNodeType | ObjectMemberNodeType | ClassMemberNodeType | ModuleMemberNodeType // module structure | "module" | "doc_comment" | "modifier_list" | "amends_clause" | "extends_clause" | "module_declaration" | "module_definition" | "annotation" | "qualified_identifier" | "import" | "import_alias" | "import_list" | "typealias_header" | "typealias_body" | "class_header" | "class_header_extends" | "class_body" | "class_body_elements" | "method_header" | "method_body" | "class_property_header" | "class_property_header_begin" | "class_property_body" | "object_body" | "object_member_elements" | "parameter" | "type_annotation" | "parameter_list" | "parameter_list_elements" | "type_parameter_list" | "type_parameter_list_elements" | "argument_list" | "argument_list_elements" | "type_argument_list" | "type_argument_list_elements" | "object_parameter_list" | "type_parameter" | "string_constant" // parts of members | "object_property_header" | "object_property_header_begin" | "object_property_body" | "object_entry_header" | "member_predicate_header" | "when_generator_header" | "for_generator_header" | "for_generator_header_definition" | "for_generator_header_definition_header" // parts of expressions | "new_header" | "function_literal_body" | "if_header" | "if_condition" | "if_condition_expr" | "if_then_expr" | "if_else_expr" | "let_parameter_definition" | "let_parameter" // parts of types | "function_type_parameters" | "parenthesized_type_elements" | "constrained_type_constraint" | "constrained_type_elements" /// The [NodeType]s that describe a terminal: a node that spans a single token of the grammar, such /// as a keyword, a punctuation character, an identifier or a literal. /// /// A terminal never has [Node.children]. typealias TerminalNodeType = // keywords "amends_keyword" | "as_keyword" | "class_keyword" | "else_keyword" | "extends_keyword" | "for_keyword" | "function_keyword" | "if_keyword" | "import_keyword" | "import_star_keyword" | "in_keyword" | "let_keyword" | "module_keyword" | "new_keyword" | "out_keyword" | "read_keyword" | "read_question_keyword" | "read_star_keyword" | "super_keyword" | "throw_keyword" | "trace_keyword" | "typealias_keyword" | "when_keyword" // punctuation | "arrow" | "assign" | "at" | "colon" | "comma" | "dot" | "gt" | "lbrace" | "lbrack" | "lparen" | "lpred" | "lt" | "minus" | "not" | "qspread" | "question" | "rbrace" | "rbrack" | "rparen" | "spread" | "star" | "underscore" | "union" // string delimiters | "interpolation_start" | "string_start" | "string_multi_start" | "string_end" // string content | "string_chars" | "string_newline" | "string_continuation" | "string_escape" // affixes | "shebang" | "line_comment" | "block_comment" | "semicolon" | "doc_comment_line" // names, modifiers and operators | "identifier" | "modifier" | "operator" // literals | "bool_literal_expr" | "int_literal_expr" | "float_literal_expr" // expressions without operands | "this_expr" | "outer_expr" | "module_expr" | "null_expr" // types without operands | "unknown_type" | "nothing_type" | "module_type" | "this_type" /// The [NodeType]s that describe an expression. typealias ExprNodeType = "this_expr" | "outer_expr" | "module_expr" | "null_expr" | "throw_expr" | "trace_expr" | "import_expr" | "read_expr" | "new_expr" | "unary_minus_expr" | "logical_not_expr" | "function_literal_expr" | "parenthesized_expr" | "super_subscript_expr" | "super_access_expr" | "subscript_expr" | "qualified_access_expr" | "if_expr" | "let_expr" | "bool_literal_expr" | "int_literal_expr" | "float_literal_expr" | "single_line_string_literal_expr" | "multi_line_string_literal_expr" | "unqualified_access_expr" | "non_null_expr" | "amends_expr" | "binary_op_expr" /// The [NodeType]s that describe a type. typealias TypeNodeType = "unknown_type" | "nothing_type" | "module_type" | "this_type" | "union_type" | "function_type" | "parenthesized_type" | "declared_type" | "nullable_type" | "string_literal_type" | "constrained_type" /// The [NodeType]s that describe a member of an object body. typealias ObjectMemberNodeType = "object_element" | "object_property" | "object_method" | "member_predicate" | "object_entry" | "object_spread" | "when_generator" | "for_generator" /// The [NodeType]s that describe a member of a class body. typealias ClassMemberNodeType = "class_property" | "class_method" /// The [NodeType]s that describe a member of a module. typealias ModuleMemberNodeType = "class_property" | "class_method" | "class" | "typealias"