Add pkl:syntax module #1569
Conversation
|
Have you considered offering the equivalent Java library? Wrapping that library instead of implementing |
|
The major thing missing from this PR is the ability to whole-cloth construct and then format |
|
The Java library is already |
As far as I know, external members of non-external classes can still be implemented in Java. If this stdlib module is implemented in Pkl, it should not be cached (as it is now), as this is very likely to cause multithreading issues. |
I don't think there would be much difference performance wise in implementing it in java. All the parsing and formatting is already in java. The Pkl classes are just a sugar layer.
What do you mean by cached? |
| let (result = parse("x: \(typeSource) = 0")) | ||
| (result as syntax.ModuleNode).properties.first.typeAnnotation!!.type | ||
|
|
||
| examples { |
There was a problem hiding this comment.
Should these be facts instead since they're all booleans?
| private static final Identifier TYPE_ID = Identifier.get("type"); | ||
| private static final Identifier CHILDREN_ID = Identifier.get("children"); | ||
| private static final Identifier SPAN_ID = Identifier.get("span"); | ||
| private static final Identifier LINE_START_ID = Identifier.get("lineStart"); | ||
| private static final Identifier COL_START_ID = Identifier.get("colStart"); | ||
| private static final Identifier LINE_END_ID = Identifier.get("lineEnd"); | ||
| private static final Identifier COL_END_ID = Identifier.get("colEnd"); |
There was a problem hiding this comment.
These should probably be members of Identifier with everything else.
| /// Parse the string as a Pkl module, returning either a typed AST node or an error. | ||
| function parse(source: String): ModuleNode | ParserError = | ||
| let (result = parseNodes(source)) | ||
| if (result is ParserError) | ||
| result | ||
| else | ||
| new ModuleNode { node = result } | ||
|
|
||
| /// Parse a resource as a Pkl module, returning either a typed AST node or an error. | ||
| function parseResource(resourceURI: String): ModuleNode | ParserError = | ||
| parse(read(resourceURI).text) |
There was a problem hiding this comment.
I've found that it's better for parser APIs to accept Resource directly to avoid confusion when parseResource("./foo.pkl") doesn't work as expected because the read is relative to pkl:syntax (which won't work).
| /// Parse the string as a Pkl module, returning either a typed AST node or an error. | |
| function parse(source: String): ModuleNode | ParserError = | |
| let (result = parseNodes(source)) | |
| if (result is ParserError) | |
| result | |
| else | |
| new ModuleNode { node = result } | |
| /// Parse a resource as a Pkl module, returning either a typed AST node or an error. | |
| function parseResource(resourceURI: String): ModuleNode | ParserError = | |
| parse(read(resourceURI).text) | |
| /// Parse the string as a Pkl module, returning either a typed AST node or an error. | |
| function parse(source: String | Resource): ModuleNode | ParserError = | |
| let (src = if (source is String) source else source.text) | |
| let (result = parseNodes(source)) | |
| if (result is ParserError) | |
| result | |
| else | |
| new ModuleNode { node = result } |
(or parseNodes should also accept a Resource as well and parse just passes it through)
| /// Affix nodes (comments, semicolons) to prepend before this node. | ||
| prefixes: List<Node> | ||
|
|
||
| /// Affix nodes (comments, semicolons) to append after this node. | ||
| suffixes: List<Node> |
There was a problem hiding this comment.
I'm not seeing if/how these properties are used
| suffixes: List<Node> | ||
|
|
||
| /// Build the typed syntax node. | ||
| abstract function build(): SyntaxNode |
There was a problem hiding this comment.
Shouldn't this be a fixed property instead?
| abstract function build(): SyntaxNode | |
| abstract fixed result: SyntaxNode |
Not drawn to a specific name here. result and syntaxNode both seem reasonable to me.
| extractStringConstant(sc) | ||
|
|
||
| /// Base class for all typed syntax nodes. | ||
| abstract class SyntaxNode { |
There was a problem hiding this comment.
Why isn't toBuilder()/builder defined on SyntaxNode?
| // Find first child of a given type within a node. | ||
| local const function findChild(n: Node, t: NodeType): Node? = | ||
| n.children.findOrNull((c) -> c.type == t) | ||
|
|
||
| // Find all children of a given type. | ||
| local const function findChildren(n: Node, t: NodeType): List<Node> = | ||
| n.children.filter((c) -> c.type == t) |
There was a problem hiding this comment.
Any particular reason not to implement all of these as methods on node. I don't see a reason not to make them public API.
| children = | ||
| (if (self.declaration == null) List() else List(self.declaration.build().node)) | ||
| + ( | ||
| if (self.imports.isEmpty) | ||
| List() | ||
| else | ||
| List(new Node { | ||
| type = "import_list" | ||
| children = self.imports.map((i) -> i.build().node) | ||
| }) | ||
| ) | ||
| + self.classes.map((c) -> c.build().node) | ||
| + self.typeAliases.map((t) -> t.build().node) | ||
| + self.properties.map((p) -> p.build().node) | ||
| + self.methods.map((m) -> m.build().node) |
There was a problem hiding this comment.
Nit: style. I ted to find List.filterNonNull makes things like this a little easier to read and maintain.
| children = | |
| (if (self.declaration == null) List() else List(self.declaration.build().node)) | |
| + ( | |
| if (self.imports.isEmpty) | |
| List() | |
| else | |
| List(new Node { | |
| type = "import_list" | |
| children = self.imports.map((i) -> i.build().node) | |
| }) | |
| ) | |
| + self.classes.map((c) -> c.build().node) | |
| + self.typeAliases.map((t) -> t.build().node) | |
| + self.properties.map((p) -> p.build().node) | |
| + self.methods.map((m) -> m.build().node) | |
| List( | |
| self.declaration?.build().node, | |
| if (self.imports.isEmpty) | |
| null | |
| else | |
| new Node { | |
| type = "import_list" | |
| children = self.imports.map((i) -> i.build().node) | |
| }, | |
| ).filterNonNull() | |
| + self.classes.map((c) -> c.build().node) | |
| + self.typeAliases.map((t) -> t.build().node) | |
| + self.properties.map((p) -> p.build().node) | |
| + self.methods.map((m) -> m.build().node) |
Or better yet, throw together a helper method:
function buildList(items: List): List = items.flatMap((it) ->
if (it is List) it
else if (it == null) List()
else List(it)
)Then
| children = | |
| (if (self.declaration == null) List() else List(self.declaration.build().node)) | |
| + ( | |
| if (self.imports.isEmpty) | |
| List() | |
| else | |
| List(new Node { | |
| type = "import_list" | |
| children = self.imports.map((i) -> i.build().node) | |
| }) | |
| ) | |
| + self.classes.map((c) -> c.build().node) | |
| + self.typeAliases.map((t) -> t.build().node) | |
| + self.properties.map((p) -> p.build().node) | |
| + self.methods.map((m) -> m.build().node) | |
| children = | |
| buildList(List( | |
| self.declaration?.build().node, | |
| if (self.imports.isEmpty) | |
| null | |
| else | |
| new Node { | |
| type = "import_list" | |
| children = self.imports.map((i) -> i.build().node) | |
| }, | |
| self.classes.map((c) -> c.build().node), | |
| self.typeAliases.map((t) -> t.build().node), | |
| self.properties.map((p) -> p.build().node), | |
| self.methods.map((m) -> m.build().node), | |
| )) |
Varargs would make this even nicer, but probably not worth it here since those are special-cased in AstBuilder.
Part of me also wonders if paying the object tax is worth it here for the readability benefit:
| children = | |
| (if (self.declaration == null) List() else List(self.declaration.build().node)) | |
| + ( | |
| if (self.imports.isEmpty) | |
| List() | |
| else | |
| List(new Node { | |
| type = "import_list" | |
| children = self.imports.map((i) -> i.build().node) | |
| }) | |
| ) | |
| + self.classes.map((c) -> c.build().node) | |
| + self.typeAliases.map((t) -> t.build().node) | |
| + self.properties.map((p) -> p.build().node) | |
| + self.methods.map((m) -> m.build().node) | |
| children = new Listing { | |
| when (self.declaration != null) { | |
| self.declaration.build().node | |
| } | |
| when (self.imports.isNotEmpty) { | |
| new Node { | |
| type = "import_list" | |
| children = self.imports.map((i) -> i.build().node) | |
| } | |
| } | |
| ...self.classes.map((c) -> c.build().node) | |
| ...self.typeAliases.map((t) -> t.build().node) | |
| ...self.properties.map((p) -> p.build().node) | |
| ...self.methods.map((m) -> m.build().node) | |
| }.toList() |
| + self.classes.map((c) -> c.build().node) | ||
| + self.typeAliases.map((t) -> t.build().node) | ||
| + self.properties.map((p) -> p.build().node) | ||
| + self.methods.map((m) -> m.build().node) |
There was a problem hiding this comment.
This will necessarily rearrange a module's members, producing all classes, then aliases, then properties, and finally methods. This seems less than ideal from a round-tripping perspective. As an alternative, it might be better to use an approach similar to object members for classes and modules.
| local function formatExpr(builder: syntax.ExprBuilder): String = | ||
| syntax.format(builder.build().node) | ||
|
|
||
| local function formatType(builder: syntax.TypeBuilder): String = | ||
| syntax.format(builder.build().node) | ||
|
|
||
| local function formatBody(builder: syntax.ObjectBodyBuilder): String = | ||
| syntax.format(builder.build().node) | ||
|
|
||
| local function formatModule(builder: syntax.ModuleBuilder): String = | ||
| syntax.format(builder.build().node) |
There was a problem hiding this comment.
Seems odd to repeat this verbatim:
| local function formatExpr(builder: syntax.ExprBuilder): String = | |
| syntax.format(builder.build().node) | |
| local function formatType(builder: syntax.TypeBuilder): String = | |
| syntax.format(builder.build().node) | |
| local function formatBody(builder: syntax.ObjectBodyBuilder): String = | |
| syntax.format(builder.build().node) | |
| local function formatModule(builder: syntax.ModuleBuilder): String = | |
| syntax.format(builder.build().node) | |
| local function format(builder: syntax.Builder): String = | |
| syntax.format(builder.build().node) |
This new stdlib module allows for full manipulation of Pkl source code in Pkl: parsing text to a Pkl CST and formatting a Pkl CST back to text.