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:
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;
|
||||
}
|
||||
|
||||
+35
@@ -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}.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
key = "hello"
|
||||
|
||||
const result = new {
|
||||
[this.key] = "world"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
key = List("hello", "world")
|
||||
|
||||
const result = new {
|
||||
for (key in this.key) {
|
||||
key
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
baz = 1
|
||||
|
||||
// no error
|
||||
const foo = let (x = 1) this.baz
|
||||
+3
@@ -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()
|
||||
+18
@@ -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)
|
||||
+18
@@ -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)
|
||||
+22
@@ -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)
|
||||
+18
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user