Refine requirements around const and this type (#1962)

This introduces a rule that const properties cannot use the `this` type if that referenced type exceeds the scope.

This code is an error:

```pkl
const foo: this
```

This is not:

```pkl
const foo = new {
  local myself: this
}
```

Additionally, this fixes:

* const depth logic to account for eager scopes
* behavior of `this` type inside custom this scopes
This commit is contained in:
Daniel Chao authored and GitHub committed 2026-11-04 11:30:50 -08:00
1 parent 7e8e5f648d
commit 1f5c62ecef
29 files changed
+273 -29

No files matched your search

@@ -125,6 +125,7 @@ import org.pkl.core.ast.expression.member.ReadQualifiedLocalPropertyNode;
import org.pkl.core.ast.expression.member.ReadSuperEntryNodeGen;
import org.pkl.core.ast.expression.member.ReadSuperPropertyNodeGen;
import org.pkl.core.ast.expression.primary.ExecuteCustomThisWithRootNode;
import org.pkl.core.ast.expression.primary.GetCustomThisReceiverClassNode;
import org.pkl.core.ast.expression.primary.GetEnclosingReceiverNode;
import org.pkl.core.ast.expression.primary.GetMemberKeyNode;
import org.pkl.core.ast.expression.primary.GetModuleNode;
@@ -434,13 +435,24 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
@Override
public UnresolvedTypeNode visitThisType(ThisType type) {
var sourceSection = createSourceSection(type);
var currentScope = symbolTable.getCurrentScope();
if (!currentScope.isCustomThisScope()
&& currentScope.getConstLevel() == ConstLevel.ALL
&& currentScope.getConstDepth() == -1) {
var message =
currentScope.isPropertyScope() ? "invalidThisTypeInProperty" : "invalidThisTypeInMethod";
throw exceptionBuilder()
.evalError(message, currentScope.getQualifiedName())
.withSourceSection(sourceSection)
.build();
}
// need to pass explicit class name for property and method arg/return type annotations.
// this is because type annotations on class properties/methods are initialized when the
// ClassNode
// is executed, and the frame's receiver is the enclosing module rather than the class.
// do not need: when in any object or at the module level (where `this` is the receiver's class)
org.pkl.core.runtime.Identifier className = null;
for (var scope = symbolTable.getCurrentScope(); scope != null; scope = scope.getParent()) {
for (var scope = currentScope; scope != null; scope = scope.getParent()) {
if (scope.isObjectScope() || scope.isCustomThisScope()) {
break;
}
@@ -462,7 +474,10 @@ public class AstBuilder extends AbstractAstBuilder<Object> {
if (isBaseModule && className != null) {
getClassNode = new GetBaseModuleClassNode(className);
} else if (className == null) {
getClassNode = new GetReceiverClassNode(sourceSection);
getClassNode =
currentScope.isCustomThisScope()
? new GetCustomThisReceiverClassNode(sourceSection)
: new GetReceiverClassNode(sourceSection);
} else if (className.isLocalProp()) {
getClassNode =
new ReadQualifiedLocalPropertyNode(
@@ -355,18 +355,29 @@ public final class SymbolTable {
*/
public int getConstDepth() {
var depth = -1;
var lexicalScope = getLexicalScope();
while (lexicalScope.getConstLevel() == ConstLevel.ALL) {
var skipObjectBody = false;
for (var scope = this;
scope != null && scope.getConstLevel() == ConstLevel.ALL;
scope = scope.getParent()) {
if (scope instanceof EagerGeneratorScope) {
skipObjectBody = true;
continue;
}
// LambdaScope inherits constLevel but doesn't create a const scope barrier.
// Method scopes don't level up.
if (!(lexicalScope instanceof LambdaScope) && !(lexicalScope instanceof MethodScope)) {
// Method scopes, let expression scopes, and for generator scopes don't level up.
if (scope instanceof LambdaScope
|| scope instanceof MethodScope
|| scope instanceof LetExpressionScope
|| scope instanceof ForGeneratorScope) {
continue;
}
if (scope instanceof LexicalScope) {
if (scope instanceof ObjectScope && skipObjectBody) {
skipObjectBody = false;
continue;
}
depth += 1;
}
var parent = lexicalScope.getParent();
if (parent == null) {
return depth;
}
lexicalScope = parent.getLexicalScope();
}
return depth;
}
@@ -0,0 +1,35 @@
/*
* 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.core.ast.expression.primary;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.source.SourceSection;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.runtime.VmUtils;
public class GetCustomThisReceiverClassNode extends ExpressionNode {
@Child ExpressionNode customThisNode;
public GetCustomThisReceiverClassNode(SourceSection sourceSection) {
this.customThisNode = new CustomThisNode(sourceSection);
}
@Override
public Object executeGeneric(VirtualFrame frame) {
var receiver = customThisNode.executeGeneric(frame);
return VmUtils.getClass(receiver);
}
}
@@ -1283,5 +1283,11 @@ Cannot reference `module` type within a type alias body.
invalidThisTypeInTypeAlias=\
Cannot reference `this` type within a type alias body.
invalidThisTypeInProperty=\
Cannot reference `this` type from const property `{0}`.
invalidThisTypeInMethod=\
Cannot reference `this` type from const method `{0}`.
cannotParseNetrc=\
Cannot parse .netrc: {0}.
@@ -0,0 +1,5 @@
key = "hello"
const result = new {
[this.key] = "world"
}
@@ -0,0 +1,7 @@
key = List("hello", "world")
const result = new {
for (key in this.key) {
key
}
}
@@ -0,0 +1,4 @@
baz = 1
// no error
const foo = let (x = 1) this.baz
@@ -0,0 +1,3 @@
baz = 1
const foo = new Listing { for (i in List(1)) { baz } }
@@ -58,8 +58,7 @@ res11d = res11.res6a.x
res12 = test.catch(() -> res11.fun(module)[0].x)
// this type not affected by custom this scope:
hidden res13: List(this is List<this>) = List(module)
hidden res13: List(this is this) = List(module)
res13a = res13[0].x
// test generic typealias through inheritance from module
@@ -0,0 +1,6 @@
qux = "qux"
const foo = new {
// `myself` here is the module; this escapes the const scope
[let (myself: this = this) myself.qux] = 5
}
@@ -0,0 +1 @@
const func = (elem: this) -> elem
@@ -0,0 +1 @@
const foo: String(this is this) = "x"
@@ -7,15 +7,22 @@ res1 = foo.length
local baz: Listing = new {
new Listing { "hello" }
new Mapping { ["hello"] = "world" }
module
}
res2 = (baz) {
[[this is this]] {
res2 = "blah"
}
when (module is this) {
123
}
module is this
}
local res3: Listing<Listing<String>> = new {
new { "one" }
new { "two" }
}
res4 = (res3) {
[[this is this]] {
"three"
}
}
@@ -8,15 +8,22 @@ res1 = foo.length
local baz: Listing = new {
new Listing { "hello" }
new Mapping { ["hello"] = "world" }
module
}
res2 = (baz) {
[[this is this]] {
res2 = "blah"
}
when (module is this) {
123
}
module is this
}
local res3: Listing<Listing<String>> = new {
new { "one" }
new { "two" }
}
res4 = (res3) {
[[this is this]] {
"three"
}
}
@@ -0,0 +1 @@
const foo: this
@@ -0,0 +1 @@
const function foo(bar: this) = 1
@@ -0,0 +1,14 @@
// okay to use `this` if it doesn't break the const scope
const function myFunc() = new {
local myself: this = this
result = myself.bar
bar = 3
}
const res1 = new {
local myself: this = this
result = myself.bar
bar = 3
}
res2 = myFunc()
@@ -0,0 +1,18 @@
–– Pkl Error ––
Cannot reference property `key` from here because it is not `const`.
x | [this.key] = "world"
^^^^^^^^
at constEagerScope#result (file:///$snippetsDir/input/errors/const/constEagerScope.pkl)
To fix, do either of:
1. Add modifier `const` to property `key`
2. Self-import this module, and reference this property from the import.
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,18 @@
–– Pkl Error ––
Cannot reference property `key` from here because it is not `const`.
x | for (key in this.key) {
^^^^^^^^
at constEagerScope2#result (file:///$snippetsDir/input/errors/const/constEagerScope2.pkl)
To fix, do either of:
1. Add modifier `const` to property `key`
2. Self-import this module, and reference this property from the import.
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,22 @@
–– Pkl Error ––
Cannot reference property `baz` from here because it is not `const`.
x | const foo = let (x = 1) this.baz
^^^^^^^^
at constExplicitThis2#foo (file:///$snippetsDir/input/errors/const/constExplicitThis2.pkl)
To fix, do either of:
1. Add modifier `const` to property `baz`
2. Self-import this module, and reference this property from the import.
x | const foo = let (x = 1) this.baz
^^^^^^^^^^^^^^^^^^^^
at constExplicitThis2#foo.<let expr> (file:///$snippetsDir/input/errors/const/constExplicitThis2.pkl)
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,18 @@
–– Pkl Error ––
Cannot reference property `baz` from here because it is not `const`.
x | const foo = new Listing { for (i in List(1)) { baz } }
^^^
at constForGenerator#foo[#1] (file:///$snippetsDir/input/errors/const/constForGenerator.pkl)
To fix, do either of:
1. Add modifier `const` to property `baz`
2. Self-import this module, and reference this property from the import.
xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
@@ -0,0 +1,6 @@
–– Pkl Error ––
Cannot reference `this` type from const method `thisType10#foo`.
x | [let (myself: this = this) myself.qux] = 5
^^^^
at thisType10#foo (file:///$snippetsDir/input/types/thisType10.pkl)
@@ -0,0 +1,6 @@
–– Pkl Error ––
Cannot reference `this` type from const method `thisType11#func.<function#1>`.
x | const func = (elem: this) -> elem
^^^^
at thisType11#func.<function#1> (file:///$snippetsDir/input/types/thisType11.pkl)
@@ -0,0 +1 @@
foo = "x"
@@ -6,10 +6,16 @@ res2 {
new {
["hello"] = "world"
}
new {
res1 = 1
res2 = "blah"
}
123
false
}
res4 {
new {
"one"
"three"
}
new {
"two"
"three"
}
}
@@ -6,10 +6,16 @@ res2 {
new {
["hello"] = "world"
}
new {
res1 = 1
res2 = "blah"
}
123
false
}
res4 {
new {
"one"
"three"
}
new {
"two"
"three"
}
}
@@ -0,0 +1,6 @@
–– Pkl Error ––
Cannot reference `this` type from const property `thisType7#foo`.
x | const foo: this
^^^^
at thisType7#foo (file:///$snippetsDir/input/types/thisType7.pkl)
@@ -0,0 +1,6 @@
–– Pkl Error ––
Cannot reference `this` type from const method `thisType8#foo`.
x | const function foo(bar: this) = 1
^^^^
at thisType8#foo (file:///$snippetsDir/input/types/thisType8.pkl)
@@ -0,0 +1,8 @@
res1 {
result = 3
bar = 3
}
res2 {
result = 3
bar = 3
}