From 0dc6a8398a85544243ad8fd56d9583c4dc197810 Mon Sep 17 00:00:00 2001 From: nileshpatil6 Date: Sat, 4 Jul 2026 23:19:41 +0530 Subject: [PATCH 1/2] Respect fixed properties when converting Map or Dynamic to Typed toTyped() on Map and Dynamic built its delegating members the same way for every property, including ones marked fixed. That let a caller supply any value for a fixed property through the source Map or Dynamic, silently overriding the value fixed by the class declaration. createDelegatingMembers is now told whether it is building members for a conversion into Typed. When that is the case, fixed properties are skipped just like hidden properties already are, so the class prototype's own fixed value is used instead of whatever is in the source Map or Dynamic. The Typed to Dynamic direction is unaffected since it already reads from the evaluated Typed value. Added snippet tests in map.pkl and dynamic.pkl covering toTyped() on a class with a fixed property. Fixes #573 Signed-off-by: nileshpatil6 --- pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java | 8 +++++++- .../test/files/LanguageSnippetTests/input/api/dynamic.pkl | 6 ++++++ .../src/test/files/LanguageSnippetTests/input/api/map.pkl | 2 ++ .../files/LanguageSnippetTests/output/api/dynamic.pcf | 4 ++++ .../test/files/LanguageSnippetTests/output/api/map.pcf | 4 ++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java index 68614aa86..f446de0db 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java @@ -497,6 +497,7 @@ public EconomicMap getTypedToDynamicMembers() { if (__typedToDynamicMembers == null) { __typedToDynamicMembers = createDelegatingMembers( + false, (member) -> new UntypedObjectMemberNode( null, new FrameDescriptor(), member, new DelegateToExtraStorageObjNode())); @@ -512,6 +513,7 @@ public EconomicMap getDynamicToTypedMembers() { if (__dynamicToTypedMembers == null) { __dynamicToTypedMembers = createDelegatingMembers( + true, (member) -> TypeCheckedPropertyNodeGen.create( null, @@ -530,6 +532,7 @@ public EconomicMap getMapToTypedMembers() { if (__mapToTypedMembers == null) { __mapToTypedMembers = createDelegatingMembers( + true, (member) -> TypeCheckedPropertyNodeGen.create( null, @@ -542,7 +545,7 @@ public EconomicMap getMapToTypedMembers() { } private EconomicMap createDelegatingMembers( - Function memberNodeFactory) { + boolean isConversionToTyped, Function memberNodeFactory) { var result = EconomicMaps.create(); for (var cursor = getAllProperties().getEntries(); cursor.advance(); ) { var property = cursor.getValue(); @@ -550,6 +553,9 @@ private EconomicMap createDelegatingMembers( // Dynamic/Map->Typed conversion: Overall it seems more useful for the typed object // to inherit its prototype's value for the hidden property (e.g., Module.output). if (property.isHidden()) continue; + // Dynamic/Map->Typed conversion: a `fixed` property's value is fixed by its declaration, + // so it must not be overridden by whatever value happens to be in the source Dynamic/Map. + if (isConversionToTyped && property.isFixed()) continue; var name = cursor.getKey(); var member = diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/dynamic.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/dynamic.pkl index 097dca131..ee6954896 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/dynamic.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/dynamic.pkl @@ -57,6 +57,7 @@ examples { module.catch(() -> new Dynamic { name = "Pigeon" }.toTyped(Person).age) module.catch(() -> obj.toTyped(Pair)) // Pair is not a Typed module.catch(() -> obj.toTyped(ValueRenderer)) // ValueRenderer is abstract + (new Dynamic { name = "Pigeon"; id = 99 }).toTyped(FixedPerson) } } @@ -89,3 +90,8 @@ local class Person { name: String = "Default" age: UInt } + +local class FixedPerson { + name: String = "Default" + fixed id: Int = 1 +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/map.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/map.pkl index 4a178cb1a..99ffb313f 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/map.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/map.pkl @@ -137,8 +137,10 @@ examples { module.catch(() -> Map("name", "Pigeon").toTyped(Person).age) module.catch(() -> Map().toTyped(Int)) module.catch(() -> Map().toTyped(Abstract)) + Map("name", "Pigeon", "id", 99).toTyped(FixedPerson) } } local class Person { name: String = "Default"; age: Int } local abstract class Abstract +local class FixedPerson { name: String = "Default"; fixed id: Int = 1 } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/dynamic.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/dynamic.pcf index adc537afb..f65b947a2 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/api/dynamic.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/dynamic.pcf @@ -57,5 +57,9 @@ examples { "Tried to read property `age` but its value is undefined." "Class `Pair` is not a subtype of `Typed`." "Cannot instantiate abstract class `ValueRenderer`." + new { + name = "Pigeon" + id = 1 + } } } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/map.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/map.pcf index 785937863..fe5057ab6 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/api/map.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/map.pcf @@ -129,5 +129,9 @@ examples { "Tried to read property `age` but its value is undefined." "Class `Int` is not a subtype of `Typed`." "Cannot instantiate abstract class `map#Abstract`." + new { + name = "Pigeon" + id = 1 + } } } From ed6cfac1cb41f5bfc44e3003f5a374ebcb90ce50 Mon Sep 17 00:00:00 2001 From: nileshpatil6 Date: Sun, 5 Jul 2026 19:51:37 +0530 Subject: [PATCH 2/2] Also respect const properties when converting Map or Dynamic to Typed const properties have the same override-immunity requirement as fixed properties: a value supplied by the source Map/Dynamic must not replace the class's own declared value. Change the isFixed() check in createDelegatingMembers to isConstOrFixed(), and add matching test cases alongside the existing fixed ones in map.pkl and dynamic.pkl. Signed-off-by: nileshpatil6 --- pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java | 7 ++++--- .../test/files/LanguageSnippetTests/input/api/dynamic.pkl | 6 ++++++ .../src/test/files/LanguageSnippetTests/input/api/map.pkl | 2 ++ .../test/files/LanguageSnippetTests/output/api/dynamic.pcf | 4 ++++ .../src/test/files/LanguageSnippetTests/output/api/map.pcf | 4 ++++ 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java index f446de0db..1cbc68c6c 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java @@ -553,9 +553,10 @@ private EconomicMap createDelegatingMembers( // Dynamic/Map->Typed conversion: Overall it seems more useful for the typed object // to inherit its prototype's value for the hidden property (e.g., Module.output). if (property.isHidden()) continue; - // Dynamic/Map->Typed conversion: a `fixed` property's value is fixed by its declaration, - // so it must not be overridden by whatever value happens to be in the source Dynamic/Map. - if (isConversionToTyped && property.isFixed()) continue; + // Dynamic/Map->Typed conversion: a `fixed` or `const` property's value is fixed by its + // declaration, so it must not be overridden by whatever value happens to be in the source + // Dynamic/Map. + if (isConversionToTyped && property.isConstOrFixed()) continue; var name = cursor.getKey(); var member = diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/dynamic.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/dynamic.pkl index ee6954896..2ab0e22cd 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/dynamic.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/dynamic.pkl @@ -58,6 +58,7 @@ examples { module.catch(() -> obj.toTyped(Pair)) // Pair is not a Typed module.catch(() -> obj.toTyped(ValueRenderer)) // ValueRenderer is abstract (new Dynamic { name = "Pigeon"; id = 99 }).toTyped(FixedPerson) + (new Dynamic { name = "Pigeon"; id = 99 }).toTyped(ConstPerson) } } @@ -95,3 +96,8 @@ local class FixedPerson { name: String = "Default" fixed id: Int = 1 } + +local class ConstPerson { + name: String = "Default" + const id: Int = 1 +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/map.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/map.pkl index 99ffb313f..bc375fe14 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/api/map.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/map.pkl @@ -138,9 +138,11 @@ examples { module.catch(() -> Map().toTyped(Int)) module.catch(() -> Map().toTyped(Abstract)) Map("name", "Pigeon", "id", 99).toTyped(FixedPerson) + Map("name", "Pigeon", "id", 99).toTyped(ConstPerson) } } local class Person { name: String = "Default"; age: Int } local abstract class Abstract local class FixedPerson { name: String = "Default"; fixed id: Int = 1 } +local class ConstPerson { name: String = "Default"; const id: Int = 1 } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/dynamic.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/dynamic.pcf index f65b947a2..6e4627be4 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/api/dynamic.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/dynamic.pcf @@ -61,5 +61,9 @@ examples { name = "Pigeon" id = 1 } + new { + name = "Pigeon" + id = 1 + } } } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/map.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/map.pcf index fe5057ab6..4fd901c1b 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/api/map.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/map.pcf @@ -133,5 +133,9 @@ examples { name = "Pigeon" id = 1 } + new { + name = "Pigeon" + id = 1 + } } }