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 6e74fc170..c5442b316 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 @@ -288,6 +288,10 @@ public AstBuilder( isMethodReturnTypeChecked = !isStdLibModule || IoUtils.isTestMode(); } + public ModuleInfo getModuleInfo() { + return moduleInfo; + } + public static AstBuilder create( Source source, VmLanguage language, @@ -311,7 +315,14 @@ public static AstBuilder create( var moduleName = IoUtils.inferModuleName(moduleKey); moduleInfo = new ModuleInfo( - sourceSection, headerSection, null, moduleName, moduleKey, resolvedModuleKey, false); + sourceSection, + headerSection, + null, + moduleName, + moduleKey, + resolvedModuleKey, + false, + null); } else { var declaredModuleName = moduleDecl.getName(); var moduleName = @@ -320,6 +331,20 @@ public static AstBuilder create( : IoUtils.inferModuleName(moduleKey); var clause = moduleDecl.getExtendsOrAmendsDecl(); var isAmend = clause != null && clause.getType() == ExtendsOrAmendsClause.Type.AMENDS; + + // if this is an amending module, resolve the module being amended + ModuleKey amendedModuleKey = null; + if (isAmend) { + try { + var amendedModuleUri = URI.create(clause.getUrl().getString()); + if (!amendedModuleUri.isAbsolute()) { + amendedModuleUri = resolvedModuleKey.getUri().resolve(amendedModuleUri); + } + amendedModuleKey = moduleResolver.resolve(amendedModuleUri, null); + } catch (Exception ignored) { + } + } + moduleInfo = new ModuleInfo( sourceSection, @@ -328,7 +353,8 @@ public static AstBuilder create( moduleName, moduleKey, resolvedModuleKey, - isAmend); + isAmend, + amendedModuleKey); } return new AstBuilder(source, language, moduleInfo, moduleResolver); 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 33da5e19e..6ea1fb11f 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 @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2025 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. @@ -23,6 +23,7 @@ import org.graalvm.collections.EconomicMap; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.ast.member.ObjectMember; +import org.pkl.core.ast.type.ResolveDeclaredTypeNode; import org.pkl.core.ast.type.UnresolvedTypeNode; import org.pkl.core.runtime.ModuleInfo; import org.pkl.core.runtime.VmLanguage; @@ -63,10 +64,17 @@ protected VmTyped eval(VirtualFrame frame, VmTyped supermodule) { .build(); } - checkIsValidTypedAmendment(supermodule); + var _supermodule = supermodule; + if (_supermodule.isNotInitialized()) { + _supermodule = ResolveDeclaredTypeNode.findPrototypeModule(this, _supermodule); + if (_supermodule == null) { + throw exceptionBuilder().evalError("cyclicalModuleLoading").build(); + } + } + checkIsValidTypedAmendment(_supermodule); - module.lateInitVmClass(supermodule.getVmClass()); - module.lateInitParent(supermodule); + module.lateInitVmClass(_supermodule.getVmClass()); + module.lateInitParent(_supermodule); module.addProperties(members); module.setExtraStorage(moduleInfo); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java index 969567655..5a77d62c5 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java @@ -99,7 +99,10 @@ public VmClass executeGeneric(VirtualFrame frame) { // nodes // via static final fields without having to fear recursive field initialization. prototype = module; - prototype.setExtraStorage(moduleInfo); + // Only set ModuleInfo if not already set (to handle cyclic dependencies) + if (!prototype.hasExtraStorage()) { + prototype.setExtraStorage(moduleInfo); + } prototype.addProperties(prototypeMembers); } else { prototype = diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java index cdf281b70..0953d0cb0 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/ResolveDeclaredTypeNode.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2025 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. @@ -16,9 +16,11 @@ package org.pkl.core.ast.type; import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.source.SourceSection; import org.pkl.core.ast.ExpressionNode; import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmLanguage; import org.pkl.core.runtime.VmObjectLike; import org.pkl.core.runtime.VmTyped; import org.pkl.core.util.Nullable; @@ -73,11 +75,38 @@ protected VmTyped getImport( var result = module.getCachedValue(importName); if (result == null) { result = callNode.call(member.getCallTarget(), module, module, importName); + + var importedModule = (VmTyped) result; + if (importedModule.isNotInitialized() && importedModule.getModuleInfo().isAmend()) { + // this is an amending module. Try to find the prototype + var proto = findPrototypeModule(this, importedModule); + if (proto == null) { + throw exceptionBuilder() + .evalError("cannotFindModuleImport", importName) + .withSourceSection(importNameSection) + .build(); + } + return proto; + } + module.setCachedValue(importName, result); } return (VmTyped) result; } + public static @Nullable VmTyped findPrototypeModule(Node node, VmTyped notInitializedModule) { + VmTyped amendingModule = null; + var moduleInfo = notInitializedModule.getModuleInfo(); + var amendedModuleKey = moduleInfo.getAmendedModuleKey(); + + while (amendedModuleKey != null) { + amendingModule = VmLanguage.get(node).loadModule(amendedModuleKey, node); + moduleInfo = amendingModule.getModuleInfo(); + amendedModuleKey = moduleInfo.getAmendedModuleKey(); + } + return amendingModule; + } + protected @Nullable Object getType( VmTyped module, Identifier typeName, SourceSection typeNameSection) { var member = module.getMember(typeName); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleInfo.java b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleInfo.java index 8d15fc1d6..95e7c872c 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleInfo.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleInfo.java @@ -37,6 +37,7 @@ public final class ModuleInfo { private final ModuleKey moduleKey; private final ResolvedModuleKey resolvedModuleKey; private final boolean isAmend; + private final @Nullable ModuleKey amendedModuleKey; @LateInit private List annotations; @@ -54,6 +55,26 @@ public ModuleInfo( ModuleKey moduleKey, ResolvedModuleKey resolvedModuleKey, boolean isAmend) { + this( + sourceSection, + headerSection, + docComment, + moduleName, + moduleKey, + resolvedModuleKey, + isAmend, + null); + } + + public ModuleInfo( + SourceSection sourceSection, + SourceSection headerSection, + SourceSection @Nullable [] docComment, + String moduleName, + ModuleKey moduleKey, + ResolvedModuleKey resolvedModuleKey, + boolean isAmend, + @Nullable ModuleKey amendedModuleKey) { this.sourceSection = sourceSection; this.headerSection = headerSection; @@ -62,6 +83,7 @@ public ModuleInfo( this.moduleKey = moduleKey; this.resolvedModuleKey = resolvedModuleKey; this.isAmend = isAmend; + this.amendedModuleKey = amendedModuleKey; } public void initAnnotations(List annotations) { @@ -179,4 +201,12 @@ public ModuleSchema getModuleSchema(VmTyped module) { public boolean isAmend() { return isAmend; } + + /** + * Returns the {@link ModuleKey} of the module being amended, or null if this is not an amending + * module. + */ + public @Nullable ModuleKey getAmendedModuleKey() { + return amendedModuleKey; + } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmLanguage.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmLanguage.java index bbe5cf674..b2853d935 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmLanguage.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmLanguage.java @@ -108,6 +108,10 @@ void initializeModule( var builder = AstBuilder.create( source, this, moduleContext, moduleKey, resolvedModuleKey, moduleResolver); + + // set ModuleInfo early to handle cyclic dependencies + emptyModule.setExtraStorage(builder.getModuleInfo()); + var moduleNode = builder.visitModule(moduleContext); moduleNode.getCallTarget().call(emptyModule, emptyModule); MinPklVersionChecker.check(emptyModule, importNode); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java index 7bbc1987c..9dc44b50d 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmTyped.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2025 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. @@ -60,6 +60,10 @@ public VmClass getVmClass() { return clazz; } + public boolean isNotInitialized() { + return clazz == null; + } + public @Nullable VmTyped getParent() { return (VmTyped) parent; } 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 efe3de0be..0ed506a5e 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -108,6 +108,9 @@ Class `{0}` cannot extend itself. moduleCannotAmendSelf=\ Module `{0}` cannot amend itself. +cyclicalModuleLoading=\ +Could not load cyclical module. + missingLocalPropertyValue=\ Missing property value. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/Foo.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/Foo.pkl new file mode 100644 index 000000000..a92ac3ecc --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/Foo.pkl @@ -0,0 +1,5 @@ +import "Qux.pkl" + +prop: Qux? + +typealias Bar = "bar" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/Qux.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/Qux.pkl new file mode 100644 index 000000000..cbcd10f03 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/Qux.pkl @@ -0,0 +1,3 @@ +import "amendsFooLv2.pkl" + +res: amendsFooLv2.Bar diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/amendsFoo.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/amendsFoo.pkl new file mode 100644 index 000000000..eddd4e7c4 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/amendsFoo.pkl @@ -0,0 +1 @@ +amends "Foo.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/amendsFooLv2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/amendsFooLv2.pkl new file mode 100644 index 000000000..bc488aa9c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/cycles/amendsFooLv2.pkl @@ -0,0 +1 @@ +amends "amendsFoo.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/cyclicalAmendsType.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/cyclicalAmendsType.pkl new file mode 100644 index 000000000..eff57e1dd --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/cyclicalAmendsType.pkl @@ -0,0 +1,3 @@ +import "../../input-helper/cycles/amendsFoo.pkl" + +prop: amendsFoo diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/modules/cycles.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/modules/cycles.pkl new file mode 100644 index 000000000..df0bbfd80 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/modules/cycles.pkl @@ -0,0 +1,3 @@ +import "../../input-helper/cycles/amendsFoo.pkl" + +foo: amendsFoo.Bar = "bar" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/modules/cycles2.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/modules/cycles2.pkl new file mode 100644 index 000000000..9c0929dfc --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/modules/cycles2.pkl @@ -0,0 +1,3 @@ +import "../../input-helper/cycles/amendsFooLv2.pkl" + +foo: amendsFooLv2.Bar = "bar" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cyclicalAmendsType.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cyclicalAmendsType.err new file mode 100644 index 000000000..93b19761b --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cyclicalAmendsType.err @@ -0,0 +1,6 @@ +–– Pkl Error –– +Module `Foo` cannot be extended or used as type because it amends another module. + +x | prop: amendsFoo + ^^^^^^^^^ +at cyclicalAmendsType (file:///$snippetsDir/input/errors/cyclicalAmendsType.pkl) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/modules/cycles.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/cycles.pcf new file mode 100644 index 000000000..5abc475eb --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/cycles.pcf @@ -0,0 +1 @@ +foo = "bar" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/modules/cycles2.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/cycles2.pcf new file mode 100644 index 000000000..5abc475eb --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/modules/cycles2.pcf @@ -0,0 +1 @@ +foo = "bar"