diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java index 244f2870b..1f9274d9a 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java @@ -1325,11 +1325,22 @@ public PklRootNode visitModule(Module mod) { var extendsOrAmendsClause = moduleDecl != null ? moduleDecl.getExtendsOrAmendsDecl() : null; - var supermoduleNode = - extendsOrAmendsClause == null - ? resolveBaseModuleClass( - org.pkl.core.runtime.Identifier.MODULE, BaseModule::getModuleClass) - : doVisitImport(false, extendsOrAmendsClause, extendsOrAmendsClause.getUrl()); + ExpressionNode supermoduleNode; + if (extendsOrAmendsClause == null) { + supermoduleNode = + resolveBaseModuleClass( + org.pkl.core.runtime.Identifier.MODULE, BaseModule::getModuleClass); + } else { + supermoduleNode = doVisitImport(false, extendsOrAmendsClause, extendsOrAmendsClause.getUrl()); + if (extendsOrAmendsClause.getParentTypeName() != null) { + supermoduleNode = + ReadPropertyNodeGen.create( + createSourceSection(extendsOrAmendsClause), + org.pkl.core.runtime.Identifier.get( + extendsOrAmendsClause.getParentTypeName().getValue()), + supermoduleNode); + } + } var propertyNames = CollectionUtils.newHashSet( @@ -1421,9 +1432,12 @@ private EconomicMap doVisitModuleProperties( var result = EconomicMaps.create(totalSize); for (var _import : imports) { - var member = visitImportClause(_import); - checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames); - EconomicMaps.put(result, member.getName(), member); + visitImportClause(_import) + .forEach( + (member) -> { + checkDuplicateMember(member.getName(), member.getHeaderSection(), propertyNames); + EconomicMaps.put(result, member.getName(), member); + }); } for (var clazz : classes) { @@ -1479,7 +1493,7 @@ private EconomicMap doVisitModuleProperties( } @Override - public ObjectMember visitImportClause(ImportClause imp) { + public List visitImportClause(ImportClause imp) { var importNode = doVisitImport(imp.isGlob(), imp, imp.getImportStr()); var moduleKey = moduleResolver.resolve(importNode.getImportUri()); var importName = @@ -1487,28 +1501,68 @@ public ObjectMember visitImportClause(ImportClause imp) { imp.getAlias() != null ? imp.getAlias().getValue() : IoUtils.inferModuleName(moduleKey), true); - return symbolTable.enterProperty( - importName, - ConstLevel.NONE, - scope -> { - var modifiers = VmModifier.IMPORT | VmModifier.LOCAL | VmModifier.CONST; - if (imp.isGlob()) { - modifiers = modifiers | VmModifier.GLOB; - } - var result = - new ObjectMember( - importNode.getSourceSection(), - importNode.getSourceSection(), - modifiers, - scope.getName(), - scope.getQualifiedName()); - - result.initMemberNode( - new UntypedObjectMemberNode( - language, scope.buildFrameDescriptor(), result, importNode)); + var imports = + new ArrayList( + 1 + + (imp.getDeconstructions() != null + ? imp.getDeconstructions().getDeconstructions().size() + : 0)); + imports.add( + symbolTable.enterProperty( + importName, + ConstLevel.NONE, + scope -> handleImportClauseItem(imp, importNode, null, scope))); + + if (imp.getDeconstructions() == null) return imports; + assert !imp.isGlob(); + for (var deconstruction : imp.getDeconstructions().getDeconstructions()) { + var deconstructionName = + org.pkl.core.runtime.Identifier.property( + deconstruction.getAlias() != null + ? deconstruction.getAlias().getValue() + : deconstruction.getName().getValue(), + true); + imports.add( + symbolTable.enterProperty( + deconstructionName, + ConstLevel.NONE, + scope -> + handleImportClauseItem( + imp, + importNode, + org.pkl.core.runtime.Identifier.get(deconstruction.getName().getValue()), + scope))); + } + return imports; + } + + private ObjectMember handleImportClauseItem( + ImportClause imp, + AbstractImportNode importNode, + @Nullable org.pkl.core.runtime.Identifier memberName, + SymbolTable.PropertyScope scope) { + var modifiers = VmModifier.IMPORT | VmModifier.LOCAL | VmModifier.CONST; + if (imp.isGlob()) { + modifiers = modifiers | VmModifier.GLOB; + } + var result = + new ObjectMember( + importNode.getSourceSection(), + importNode.getSourceSection(), + modifiers, + scope.getName(), + scope.getQualifiedName()); + + result.initMemberNode( + new UntypedObjectMemberNode( + language, + scope.buildFrameDescriptor(), + result, + memberName == null + ? importNode + : ReadPropertyNodeGen.create(createSourceSection(imp), memberName, importNode))); - return result; - }); + return result; } @Override diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java index 98c0aec69..de2098434 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java @@ -25,6 +25,7 @@ import org.pkl.core.ast.member.ObjectMember; import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.runtime.ModuleInfo; +import org.pkl.core.runtime.VmClass; import org.pkl.core.runtime.VmLanguage; import org.pkl.core.runtime.VmTyped; import org.pkl.core.runtime.VmUtils; @@ -78,4 +79,10 @@ protected VmTyped eval(VirtualFrame frame, VmTyped supermodule) { return module; } + + // when using `amends Foo in "bar.pkl"` + @Specialization + protected VmTyped eval(VirtualFrame frame, VmClass superclass) { + return eval(frame, superclass.getPrototype()); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java index cb1877fb7..1cfabd6e0 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/UnresolvedTypeNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-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. @@ -191,13 +191,17 @@ public TypeNode execute(VirtualFrame frame) { return new TypeAliasTypeNode(sourceSection, alias, new TypeNode[0]); } - var module = (VmTyped) type; - assert module.isModuleObject(); - var clazz = module.getVmClass(); - if (!module.isPrototype()) { - throw exceptionBuilder().evalError("notAModuleType", clazz.getModuleName()).build(); + var moduleOrClass = (VmTyped) type; + var clazz = moduleOrClass.getVmClass(); + if (!moduleOrClass.isPrototype()) { + throw exceptionBuilder() + .evalError( + "notAModuleOrClassType", + moduleOrClass.isModuleObject() ? "Module" : "Class", + clazz.getDisplayName()) + .build(); } - return TypeNode.forClass(sourceSection, module.getVmClass()); + return TypeNode.forClass(sourceSection, clazz); } } diff --git a/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java b/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java index 4cbe56085..fe426b280 100644 --- a/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java +++ b/pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java @@ -222,7 +222,7 @@ private List evaluate( var exprNode = builder.visitExpr(expr); evaluateExpr(replState, exprNode, forceResults, results); } else if (tree instanceof ImportClause importClause) { - addStaticModuleProperty(builder.visitImportClause(importClause)); + builder.visitImportClause(importClause).forEach(this::addStaticModuleProperty); } else if (tree instanceof ClassProperty classProperty) { var propertyNode = builder.visitClassProperty(classProperty); var property = addModuleProperty(propertyNode); diff --git a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties index e9a4147ea..c5af4bc18 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -49,8 +49,8 @@ Cannot invoke abstract property `{0}`. cannotInvokeSupermethodFromHere=\ Cannot invoke a supermethod from here. -notAModuleType=\ -Module `{0}` cannot be extended or used as type because it amends another module. +notAModuleOrClassType=\ +{0} `{1}` cannot be extended or used as type because it amends another module or class. invalidSupertype=\ `{0}` is not a valid supertype. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/amendModuleClass.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/amendModuleClass.pkl new file mode 100644 index 000000000..247e93cb3 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/amendModuleClass.pkl @@ -0,0 +1,3 @@ +amends Widget in ".../input/basic/imported.pkl" + +qux = 5 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/amendModuleValue.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/amendModuleValue.pkl new file mode 100644 index 000000000..f30ffc22b --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/amendModuleValue.pkl @@ -0,0 +1,3 @@ +amends baz in ".../input/basic/imported.pkl" + +qux = 5 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/extendModuleClass.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/extendModuleClass.pkl new file mode 100644 index 000000000..49cc422c5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/extendModuleClass.pkl @@ -0,0 +1,3 @@ +extends Widget in ".../input/basic/imported.pkl" + +corge = quux * qux diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/extendModuleValue.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/extendModuleValue.pkl new file mode 100644 index 000000000..8a856527e --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/modules/extendModuleValue.pkl @@ -0,0 +1 @@ +extends baz in ".../input/basic/imported.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/import4.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/import4.pkl new file mode 100644 index 000000000..24345f7af --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/import4.pkl @@ -0,0 +1,15 @@ +import "imported.pkl" as _imported, { Widget as _Widget, baz } +import "imported2.pkl" as imported2, { baz as qux, bax as quux } +import "pkl:test" + +a = _imported +b: _imported = new { + foo = 7 +} +c = baz +d: _Widget = new { + qux = 5 +} +e = test.catch(() -> new baz {}) // TODO provide a better error here than an assertion failure +f = test.catch(() -> new qux {}) // TODO provide a better error here than an assertion failure +g = test.catch(() -> new imported2 {}) // TODO provide a better error here than an assertion failure diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/import5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/import5.pkl new file mode 100644 index 000000000..1b8acef94 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/import5.pkl @@ -0,0 +1,5 @@ +import "imported.pkl" as { Widget as _Widget } + +class _Widget {} + +a = new _Widget {} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/imported.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/imported.pkl index 22790ac57..8de36e037 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/imported.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/imported.pkl @@ -1,2 +1,11 @@ foo = bar * 2 bar = 3 + +open class Widget { + qux: Int + quux: Int = qux * 3 +} + +baz: Widget = new { + qux = 4 +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/basic/imported2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/imported2.pkl new file mode 100644 index 000000000..c605c2de2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/basic/imported2.pkl @@ -0,0 +1 @@ +amends "imported.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/modules/amendModule7.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/modules/amendModule7.pkl new file mode 100644 index 000000000..7a49b661f --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/modules/amendModule7.pkl @@ -0,0 +1,4 @@ +// these don't extend Module so they have no `output` property +// thus cannot be rendered directly +amendModuleClass = import(".../input-helper/modules/amendModuleClass.pkl") +amendModuleValue = import(".../input-helper/modules/amendModuleValue.pkl") diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/modules/extendModule2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/modules/extendModule2.pkl new file mode 100644 index 000000000..936410c62 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/modules/extendModule2.pkl @@ -0,0 +1,8 @@ +import "pkl:test" + +// these don't extend Module so they have no `output` property +// thus cannot be rendered directly +extendModuleClass = (import(".../input-helper/modules/extendModuleClass.pkl")) { + qux = 2 +} +extendModuleValue = test.catch(() -> import(".../input-helper/modules/extendModuleValue.pkl")) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/import4.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/import4.pcf new file mode 100644 index 000000000..e1e609a61 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/import4.pcf @@ -0,0 +1,27 @@ +a { + foo = 6 + bar = 3 + baz { + qux = 4 + quux = 12 + } +} +b { + foo = 7 + bar = 3 + baz { + qux = 4 + quux = 12 + } +} +c { + qux = 4 + quux = 12 +} +d { + qux = 5 + quux = 15 +} +e = "Class `imported` cannot be extended or used as type because it amends another module or class." +f = "Class `imported` cannot be extended or used as type because it amends another module or class." +g = "Module `imported` cannot be extended or used as type because it amends another module or class." diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/import5.err b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/import5.err new file mode 100644 index 000000000..999fe31ba --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/import5.err @@ -0,0 +1,6 @@ +–– Pkl Error –– +Duplicate definition of member `_Widget`. + +x | class _Widget {} + ^^^^^^^^^^^^^ +at import5 (file:///$snippetsDir/input/basic/import5.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/imported.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/imported.pcf index 7249e6b0a..cc089ed28 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/imported.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/imported.pcf @@ -1,2 +1,6 @@ foo = 6 bar = 3 +baz { + qux = 4 + quux = 12 +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/basic/imported2.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/imported2.pcf new file mode 100644 index 000000000..cc089ed28 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/basic/imported2.pcf @@ -0,0 +1,6 @@ +foo = 6 +bar = 3 +baz { + qux = 4 + quux = 12 +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/modules/amendModule7.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/amendModule7.pcf new file mode 100644 index 000000000..99695c148 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/amendModule7.pcf @@ -0,0 +1,8 @@ +amendModuleClass { + qux = 5 + quux = 15 +} +amendModuleValue { + qux = 5 + quux = 15 +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/modules/amendModuleClass.err b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/amendModuleClass.err new file mode 100644 index 000000000..b158d92e9 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/amendModuleClass.err @@ -0,0 +1,6 @@ +–– Pkl Error –– +Cannot find property `output` in module `amendModuleClass`. + +Available properties in module `amendModuleClass`: +quux +qux \ No newline at end of file diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/modules/amendModuleValue.err b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/amendModuleValue.err new file mode 100644 index 000000000..e0e38c3c4 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/amendModuleValue.err @@ -0,0 +1,6 @@ +–– Pkl Error –– +Cannot find property `output` in module `amendModuleValue`. + +Available properties in module `amendModuleValue`: +quux +qux \ No newline at end of file diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/modules/extendModule2.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/extendModule2.pcf new file mode 100644 index 000000000..ee155f654 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/extendModule2.pcf @@ -0,0 +1,6 @@ +extendModuleClass { + qux = 2 + quux = 6 + corge = 12 +} +extendModuleValue = "Class `imported#Widget` cannot be extended or used as type because it amends another module or class." diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt index bbcb93a68..438007b26 100644 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt +++ b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt @@ -83,6 +83,9 @@ internal class Builder(sourceText: String, private val grammarVersion: GrammarVe NodeType.IMPORT_LIST -> formatImportList(node) NodeType.IMPORT -> formatImport(node) NodeType.IMPORT_ALIAS -> Group(newId(), formatGeneric(node.children, spaceOrLine())) + NodeType.IMPORT_DECONSTRUCTION_LIST -> formatImportDeconstructionList(node) + NodeType.IMPORT_DECONSTRUCTION_LIST_ELEMENTS -> formatImportDeconstructionListElements(node) + NodeType.IMPORT_DECONSTRUCTION -> formatImportDeconstruction(node) NodeType.CLASS -> formatClass(node) NodeType.CLASS_HEADER -> formatClassHeader(node) NodeType.CLASS_HEADER_EXTENDS -> formatClassHeaderExtends(node) @@ -370,7 +373,11 @@ internal class Builder(sourceText: String, private val grammarVersion: GrammarVe } private fun formatAmendsExtendsClause(node: Node): FormatNode { - val prefix = formatGeneric(node.children.dropLast(1), spaceOrLine()) + val prefix = + formatGenericWithGen(node.children.dropLast(1), spaceOrLine()) { elem, _ -> + if (elem.isTerminal("in") || elem.type == NodeType.IDENTIFIER) indent(format(elem)) + else format(elem) + } // string constant val suffix = Indent(listOf(format(node.children.last()))) return Group(newId(), prefix + listOf(spaceOrLine()) + suffix) @@ -385,6 +392,39 @@ internal class Builder(sourceText: String, private val grammarVersion: GrammarVe ) } + private fun formatImportDeconstructionList(node: Node): FormatNode { + val id = newId() + val nodes = + formatGeneric(node.children) { prev, next -> + if (prev.isTerminal("{") || next.isTerminal("}")) { + if (next.isTerminal("}")) { + // trailing comma + // TODO: determine if we should unconditionally add the trailing comma + // TODO since this is not valid syntax before that was introduced + if (grammarVersion == GrammarVersion.V1) { + Line + } else { + ifWrap(id, nodes(Text(","), line()), line()) + } + } else line() + } else spaceOrLine() + } + return Group(id, nodes) + } + + private fun formatImportDeconstructionListElements(node: Node): FormatNode { + return Indent(formatGeneric(node.children, spaceOrLine())) + } + + private fun formatImportDeconstruction(node: Node): FormatNode { + return Group( + newId(), + formatGenericWithGen(node.children, spaceOrLine()) { elem, _ -> + if (elem == node.firstProperChild()) format(elem) else indent(format(elem)) + }, + ) + } + private fun formatAnnotation(node: Node): FormatNode { return Group(newId(), formatGeneric(node.children, spaceOrLine())) } diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/input/imports.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/input/imports.pkl index 96f60e70e..a3d7780c2 100644 --- a/pkl-formatter/src/test/files/FormatterSnippetTests/input/imports.pkl +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/input/imports.pkl @@ -1,6 +1,9 @@ // top level comment import "@foo/Foo.pkl" as foo +import "@foo/Foo.pkl" as foo, { Bar, Baz as baz, Qux as quux, Quuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuux as quuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuux } +import "@foo/Foo.pkl" as { /* Bar */ Bar, Baz as baz /* baz */ } // qux +import "@foo/Foo.pkl" as { /* 1 */ hello, world /* 2 */, /* a */ a as b, b /* b */ as c, c as /* c */ d, d as e /* d */ } // quux import* "**.pkl" /* ** */ // glob import import "pkl:math" diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/input/module-definitions.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/input/module-definitions.pkl index 8fe82fdab..66a6760e2 100644 --- a/pkl-formatter/src/test/files/FormatterSnippetTests/input/module-definitions.pkl +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/input/module-definitions.pkl @@ -10,4 +10,6 @@ foo .bar amends +Qux +in "baz.pkl" diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/output/imports.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/output/imports.pkl index 8f9a2aa9d..7e1515987 100644 --- a/pkl-formatter/src/test/files/FormatterSnippetTests/output/imports.pkl +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/output/imports.pkl @@ -7,6 +7,26 @@ import "pkl:reflect" import "@bar/Bar.pkl" import "@foo/Foo.pkl" as foo +import + "@foo/Foo.pkl" + as foo + , { + Bar, + Baz as baz, + Qux as quux, + Quuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuux + as + quuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuux, + } +import "@foo/Foo.pkl" as { /* Bar */ Bar, Baz as baz /* baz */} // qux +import + "@foo/Foo.pkl" + as { /* 1 */ hello, + world /* 2 */ , /* a */ a as b, + b /* b */ as c, + c as /* c */ d, + d as e /* d */, + } // quux import "..." import "module1.pkl" diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/output/module-definitions.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/output/module-definitions.pkl index 9cea366f9..d09ae5edb 100644 --- a/pkl-formatter/src/test/files/FormatterSnippetTests/output/module-definitions.pkl +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/output/module-definitions.pkl @@ -4,4 +4,4 @@ /// doc comment open module foo.bar -amends "baz.pkl" +amends Qux in "baz.pkl" diff --git a/pkl-parser/src/main/java/org/pkl/parser/BaseParserVisitor.java b/pkl-parser/src/main/java/org/pkl/parser/BaseParserVisitor.java index c23d96120..9fe5f316d 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/BaseParserVisitor.java +++ b/pkl-parser/src/main/java/org/pkl/parser/BaseParserVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-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. @@ -55,6 +55,8 @@ import org.pkl.parser.syntax.ExtendsOrAmendsClause; import org.pkl.parser.syntax.Identifier; import org.pkl.parser.syntax.ImportClause; +import org.pkl.parser.syntax.ImportDeconstruction; +import org.pkl.parser.syntax.ImportDeconstructionList; import org.pkl.parser.syntax.Keyword; import org.pkl.parser.syntax.Modifier; import org.pkl.parser.syntax.ModuleDecl; @@ -357,6 +359,16 @@ public T visitImportClause(ImportClause imp) { return visitChildren(imp); } + @Override + public T visitImportDeconstructionList(ImportDeconstructionList importDeconstructionList) { + return visitChildren(importDeconstructionList); + } + + @Override + public T visitImportDeconstruction(ImportDeconstruction importDeconstruction) { + return defaultValue(); + } + @Override public T visitClass(Class clazz) { return visitChildren(clazz); diff --git a/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java b/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java index cf76a8b5f..1289a74eb 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java +++ b/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java @@ -130,6 +130,12 @@ private Node parseModuleDecl(List preChildren) { var subChildren = new ArrayList(); subChildren.add(makeTerminal(next())); ff(subChildren); + if (lookahead == Token.IDENTIFIER) { + subChildren.add(parseIdentifier()); + ff(subChildren); + expect(Token.IN, subChildren, "unexpectedToken", "in"); + ff(subChildren); + } subChildren.add(parseStringConstant()); children.add(new Node(type, subChildren)); } @@ -141,7 +147,7 @@ private Node parseQualifiedIdentifier() { children.add(parseIdentifier()); while (lookahead() == Token.DOT) { ff(children); - children.add(new Node(NodeType.TERMINAL, next().span)); + children.add(makeTerminal(next())); ff(children); children.add(parseIdentifier()); } @@ -155,15 +161,54 @@ private Node parseImportDecl() { children.add(parseStringConstant()); if (lookahead() == Token.AS) { ff(children); - var alias = new ArrayList(); - alias.add(makeTerminal(next())); - ff(alias); - alias.add(parseIdentifier()); - children.add(new Node(NodeType.IMPORT_ALIAS, alias)); + var as = makeTerminal(next()); + if (lookahead == Token.IDENTIFIER) { + var alias = new ArrayList(); + alias.add(as); // as + ff(alias); + alias.add(parseIdentifier()); + + children.add(new Node(NodeType.IMPORT_ALIAS, alias)); + ff(children); + + if (lookahead == Token.COMMA) { + children.add(parseImportDeconstructionList(makeTerminal(next()), null)); + } + } else { + children.add(parseImportDeconstructionList(as, "identifier")); + } } return new Node(NodeType.IMPORT, children); } + private Node parseImportDeconstructionList( + Node separator, @Nullable String additonalExpectation) { + var children = new ArrayList(); + children.add(separator); // as or , + ff(children); + if (additonalExpectation != null) + expect(Token.LBRACE, children, "unexpectedToken2", additonalExpectation, "{"); + else expect(Token.LBRACE, children, "unexpectedToken", "{"); + ff(children); + var elements = new ArrayList(); + parseListOf(Token.RBRACE, elements, this::parseImportDeconstruction); + children.add(new Node(NodeType.IMPORT_DECONSTRUCTION_LIST_ELEMENTS, elements)); + expect(Token.RBRACE, children, "unexpectedToken2", ",", "}"); + return new Node(NodeType.IMPORT_DECONSTRUCTION_LIST, children); + } + + private Node parseImportDeconstruction() { + var children = new ArrayList(); + children.add(parseIdentifier()); + if (lookahead() == Token.AS) { + ff(children); + children.add(makeTerminal(next())); + ff(children); + children.add(parseIdentifier()); + } + return new Node(NodeType.IMPORT_DECONSTRUCTION, children); + } + private HeaderResult parseMemberHeader(List children) { var hasDocComment = false; var hasAnnotation = false; diff --git a/pkl-parser/src/main/java/org/pkl/parser/Parser.java b/pkl-parser/src/main/java/org/pkl/parser/Parser.java index 688c90823..cd7c85faa 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/Parser.java +++ b/pkl-parser/src/main/java/org/pkl/parser/Parser.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-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. @@ -59,6 +59,8 @@ import org.pkl.parser.syntax.ExtendsOrAmendsClause; import org.pkl.parser.syntax.Identifier; import org.pkl.parser.syntax.ImportClause; +import org.pkl.parser.syntax.ImportDeconstruction; +import org.pkl.parser.syntax.ImportDeconstructionList; import org.pkl.parser.syntax.Keyword; import org.pkl.parser.syntax.Modifier; import org.pkl.parser.syntax.Module; @@ -245,19 +247,22 @@ private QualifiedIdentifier parseQualifiedIdentifier() { } private @Nullable ExtendsOrAmendsClause parseExtendsAmendsDecl() { - if (lookahead == Token.EXTENDS) { - var tk = next().span; - var url = parseStringConstant(); - return new ExtendsOrAmendsClause( - url, ExtendsOrAmendsClause.Type.EXTENDS, tk.endWith(url.span())); + var type = + lookahead == Token.EXTENDS + ? ExtendsOrAmendsClause.Type.EXTENDS + : lookahead == Token.AMENDS ? ExtendsOrAmendsClause.Type.AMENDS : null; + if (type == null) { + return null; } - if (lookahead == Token.AMENDS) { - var tk = next().span; - var url = parseStringConstant(); - return new ExtendsOrAmendsClause( - url, ExtendsOrAmendsClause.Type.AMENDS, tk.endWith(url.span())); + + var tk = next().span; + Identifier parentTypeName = null; + if (lookahead == Token.IDENTIFIER) { + parentTypeName = parseIdentifier(); + expect(Token.IN, "unexpectedToken", "in"); } - return null; + var url = parseStringConstant(); + return new ExtendsOrAmendsClause(url, parentTypeName, type, tk.endWith(url.span())); } private ImportClause parseImportDecl() { @@ -272,12 +277,50 @@ private ImportClause parseImportDecl() { var str = parseStringConstant(); var end = str.span(); Identifier alias = null; + ImportDeconstructionList deconstructions = null; + if (lookahead == Token.AS) { + next(); + if (isGlob) { + alias = parseIdentifier(); + end = alias.span(); + } else if (lookahead == Token.IDENTIFIER) { + alias = parseIdentifier(); + if (lookahead == Token.COMMA) { // alias with deconstruction(s) + next(); + deconstructions = parseImportDeconstructionList(null); + end = deconstructions.span(); + } else { // just an alias + end = alias.span(); + } + } else { // just deconstruction(s) + deconstructions = parseImportDeconstructionList("identifier"); + end = deconstructions.span(); + } + } + return new ImportClause(str, isGlob, alias, deconstructions, start.endWith(end)); + } + + private ImportDeconstructionList parseImportDeconstructionList( + @Nullable String additonalExpectation) { + var start = + additonalExpectation != null + ? expect(Token.LBRACE, "unexpectedToken2", additonalExpectation, "{").span + : expect(Token.LBRACE, "unexpectedToken", "{").span; + var deconstructions = parseListOf(Token.COMMA, Token.RBRACE, this::parseImportDeconstruction); + var end = expect(Token.RBRACE, "unexpectedToken2", ",", "}").span; + return new ImportDeconstructionList(deconstructions, start.endWith(end)); + } + + private ImportDeconstruction parseImportDeconstruction() { + var name = parseIdentifier(); + var span = name.span(); + Identifier alias = null; if (lookahead == Token.AS) { next(); alias = parseIdentifier(); - end = alias.span(); + span = span.endWith(alias.span()); } - return new ImportClause(str, isGlob, alias, start.endWith(end)); + return new ImportDeconstruction(name, alias, span); } private MemberHeader parseMemberHeader() { diff --git a/pkl-parser/src/main/java/org/pkl/parser/ParserVisitor.java b/pkl-parser/src/main/java/org/pkl/parser/ParserVisitor.java index 4badd687f..cb53e8114 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/ParserVisitor.java +++ b/pkl-parser/src/main/java/org/pkl/parser/ParserVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-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. @@ -55,6 +55,8 @@ import org.pkl.parser.syntax.ExtendsOrAmendsClause; import org.pkl.parser.syntax.Identifier; import org.pkl.parser.syntax.ImportClause; +import org.pkl.parser.syntax.ImportDeconstruction; +import org.pkl.parser.syntax.ImportDeconstructionList; import org.pkl.parser.syntax.Keyword; import org.pkl.parser.syntax.Modifier; import org.pkl.parser.syntax.Module; @@ -182,6 +184,10 @@ public interface ParserVisitor { Result visitImportClause(ImportClause imp); + Result visitImportDeconstructionList(ImportDeconstructionList importDeconstructionList); + + Result visitImportDeconstruction(ImportDeconstruction importDeconstruction); + Result visitClass(Class clazz); Result visitModifier(Modifier modifier); diff --git a/pkl-parser/src/main/java/org/pkl/parser/syntax/ExtendsOrAmendsClause.java b/pkl-parser/src/main/java/org/pkl/parser/syntax/ExtendsOrAmendsClause.java index 663fae469..0bb70333b 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/syntax/ExtendsOrAmendsClause.java +++ b/pkl-parser/src/main/java/org/pkl/parser/syntax/ExtendsOrAmendsClause.java @@ -15,7 +15,7 @@ */ package org.pkl.parser.syntax; -import java.util.List; +import java.util.Arrays; import java.util.Objects; import org.pkl.parser.ParserVisitor; import org.pkl.parser.Span; @@ -24,8 +24,9 @@ public class ExtendsOrAmendsClause extends AbstractNode { private final Type type; - public ExtendsOrAmendsClause(StringConstant url, Type type, Span span) { - super(span, List.of(url)); + public ExtendsOrAmendsClause( + StringConstant url, @Nullable Identifier parentTypeName, Type type, Span span) { + super(span, Arrays.asList(url, parentTypeName)); this.type = type; } @@ -41,6 +42,11 @@ public StringConstant getUrl() { return ret; } + public @Nullable Identifier getParentTypeName() { + assert children != null; + return (Identifier) children.get(1); + } + public Type getType() { return type; } diff --git a/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportClause.java b/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportClause.java index 156e3f7c5..4ae2b71a4 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportClause.java +++ b/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportClause.java @@ -25,8 +25,13 @@ public final class ImportClause extends AbstractNode { private final boolean isGlob; public ImportClause( - StringConstant importStr, boolean isGlob, @Nullable Identifier alias, Span span) { - super(span, Arrays.asList(importStr, alias)); + StringConstant importStr, + boolean isGlob, + @Nullable Identifier alias, + @Nullable ImportDeconstructionList deconstructions, + Span span) { + super(span, Arrays.asList(importStr, alias, deconstructions)); + assert !isGlob || deconstructions == null; // no deconstructions allowed for globs this.isGlob = isGlob; } @@ -51,6 +56,11 @@ public boolean isGlob() { return (Identifier) children.get(1); } + public @Nullable ImportDeconstructionList getDeconstructions() { + assert children != null; + return (ImportDeconstructionList) children.get(2); + } + @Override public String toString() { return "Import{isGlob=" + isGlob + ", span=" + span + ", children=" + children + '}'; diff --git a/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportDeconstruction.java b/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportDeconstruction.java new file mode 100644 index 000000000..0a2c019fd --- /dev/null +++ b/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportDeconstruction.java @@ -0,0 +1,45 @@ +/* + * 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. + */ +package org.pkl.parser.syntax; + +import java.util.Arrays; +import org.pkl.parser.ParserVisitor; +import org.pkl.parser.Span; +import org.pkl.parser.util.Nullable; + +public final class ImportDeconstruction extends AbstractNode { + + public ImportDeconstruction(Identifier name, @Nullable Identifier alias, Span span) { + super(span, Arrays.asList(name, alias)); + } + + public Identifier getName() { + assert children != null; + var name = (Identifier) children.get(0); + assert name != null; + return name; + } + + public @Nullable Identifier getAlias() { + assert children != null; + return (Identifier) children.get(1); + } + + @Override + public T accept(ParserVisitor visitor) { + return visitor.visitImportDeconstruction(this); + } +} diff --git a/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportDeconstructionList.java b/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportDeconstructionList.java new file mode 100644 index 000000000..851300a94 --- /dev/null +++ b/pkl-parser/src/main/java/org/pkl/parser/syntax/ImportDeconstructionList.java @@ -0,0 +1,38 @@ +/* + * 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. + */ +package org.pkl.parser.syntax; + +import java.util.List; +import org.pkl.parser.ParserVisitor; +import org.pkl.parser.Span; + +public final class ImportDeconstructionList extends AbstractNode { + + public ImportDeconstructionList(List parameters, Span span) { + super(span, parameters); + } + + @Override + public T accept(ParserVisitor visitor) { + return visitor.visitImportDeconstructionList(this); + } + + @SuppressWarnings("unchecked") + public List getDeconstructions() { + assert children != null; + return (List) children; + } +} diff --git a/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java b/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java index 72c136100..e35d48c9a 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java +++ b/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-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. @@ -37,6 +37,9 @@ public enum NodeType { QUALIFIED_IDENTIFIER, IMPORT, IMPORT_ALIAS, + IMPORT_DECONSTRUCTION_LIST, + IMPORT_DECONSTRUCTION_LIST_ELEMENTS, + IMPORT_DECONSTRUCTION, IMPORT_LIST, TYPEALIAS, TYPEALIAS_HEADER,