Files
pkl/stdlib/syntax.pkl

488 lines
15 KiB
Plaintext

//===----------------------------------------------------------------------===//
// 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 renderValue(node: Node): String
}
/// Renders a [Node] as an [S-expression](https://en.wikipedia.org/wiki/S-expression).
///
/// Each node is rendered as its [type][Node.type] in parentheses, followed by its
/// [children][Node.children], each on its own line and indented by two spaces.
/// Nodes without children are rendered on a single line.
///
/// Example:
/// ```
/// new SExpressionRenderer {}.renderValue(new Parser {}.parseExpression("1 + 2"))
/// ```
///
/// The above renders:
///
/// ```
/// (binary_op_expr
/// (int_literal_expr "1")
/// (operator "+")
/// (int_literal_expr "2"))
/// ```
class SExpressionRenderer {
/// Whether to render the [Node.text] of terminal nodes whose text is not implied by their
/// [Node.type].
///
/// If [true], the text follows the type as a quoted, escaped string, as in
/// `(int_literal_expr "5")`.
/// If [false], only the type is rendered.
renderTerminalText: Boolean = true
/// The types of the terminal nodes that [renderTerminalText] applies to.
local textTypes: Set<TerminalNodeType> =
Set(
"identifier",
"modifier",
"operator",
"bool_literal_expr",
"int_literal_expr",
"float_literal_expr",
"string_start",
"string_multi_start",
"string_end",
"interpolation_start",
"string_chars",
"string_escape",
"string_continuation",
"shebang",
"line_comment",
"block_comment",
"doc_comment_line",
)
/// Renders [value] as a S-expression.
function renderValue(value: Node): String = renderIndented(value, "")
local function renderIndented(node: Node, indent: String): String =
if (node.children.isEmpty)
if (renderTerminalText && textTypes.contains(node.type))
"\(indent)(\(node.type) \(new PcfRenderer {}.renderValue(node.text)))"
else
"\(indent)(\(node.type))"
else
"\(indent)(\(node.type)\n"
+ node.children.map((child) -> renderIndented(child, indent + " ")).join("\n")
+ ")"
}
/// 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<Node>(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<Result>(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<Node>
/// 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<Node>
/// 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<Node>
/// 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"