Add an s-exp renderer to pkl:syntax (#1955)

This commit is contained in:
Islon Scherer authored and GitHub committed 2026-11-05 17:10:33 +00:00
1 parent 85b4a4669a
commit fc5d658aa3
3 files changed
+223

No files matched your search

@@ -0,0 +1,58 @@
amends "../snippetTest.pkl"
import "pkl:syntax"
local renderer = new syntax.SExpressionRenderer { renderTerminalText = false }
local textRenderer = new syntax.SExpressionRenderer {}
local function parseExpr(source: String): syntax.Node =
new syntax.Parser {}.parseExpression(source)
local function parseModule(source: String): syntax.Node =
new syntax.Parser {}.parseModule(source)
facts {
["terminal"] {
renderer.renderValue(parseExpr("5")) == "(int_literal_expr)"
}
["terminal with text"] {
textRenderer.renderValue(parseExpr("5")) == #"(int_literal_expr "5")"#
}
}
examples {
["nested nodes are indented"] {
renderer.renderValue(parseExpr("1 + 2"))
}
["nested nodes with terminal text"] {
textRenderer.renderValue(parseExpr("1 + 2"))
}
["keywords and punctuation are rendered without text"] {
textRenderer.renderValue(parseExpr("if (true) x else 0"))
}
["terminal text is escaped"] {
textRenderer.renderValue(parseExpr(#""a \"b\"\n""#))
}
["module"] {
renderer.renderValue(parseModule("x = 1"))
textRenderer.renderValue(parseModule("x = 1"))
}
["class"] {
renderer.renderValue(
parseModule(
"""
class Bird {
name: String
}
""",
)
)
}
}
@@ -0,0 +1,100 @@
facts {
["terminal"] {
true
}
["terminal with text"] {
true
}
}
examples {
["nested nodes are indented"] {
"""
(binary_op_expr
(int_literal_expr)
(operator)
(int_literal_expr))
"""
}
["nested nodes with terminal text"] {
"""
(binary_op_expr
(int_literal_expr "1")
(operator "+")
(int_literal_expr "2"))
"""
}
["keywords and punctuation are rendered without text"] {
"""
(if_expr
(if_header
(if_keyword)
(if_condition
(lparen)
(if_condition_expr
(bool_literal_expr "true"))
(rparen)))
(if_then_expr
(unqualified_access_expr
(identifier "x")))
(else_keyword)
(if_else_expr
(int_literal_expr "0")))
"""
}
["terminal text is escaped"] {
"""
(single_line_string_literal_expr
(string_start "\\"")
(string_chars "a ")
(string_escape "\\\\\\"")
(string_chars "b")
(string_escape "\\\\\\"")
(string_escape "\\\\n")
(string_end "\\""))
"""
}
["module"] {
"""
(module
(class_property
(class_property_header
(class_property_header_begin
(identifier)))
(assign)
(class_property_body
(int_literal_expr))))
"""
"""
(module
(class_property
(class_property_header
(class_property_header_begin
(identifier "x")))
(assign)
(class_property_body
(int_literal_expr "1"))))
"""
}
["class"] {
"""
(module
(class
(class_header
(class_keyword)
(identifier))
(class_body
(lbrace)
(class_body_elements
(class_property
(class_property_header
(class_property_header_begin
(identifier)))
(type_annotation
(colon)
(declared_type
(qualified_identifier
(identifier))))))
(rbrace))))
"""
}
}
+65
View File
@@ -49,6 +49,71 @@ class FormattingRenderer {
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