Skip to content

Commit

Permalink
Minimize for-generator scoping bug
Browse files Browse the repository at this point in the history
There is currently a bug around resolving variables within the iterable of a for
generator or spread syntax (apple#741)

Normally, mappings/listings are type-checked lazily. However, this results in the said
bug getting widened, for any object members declared in the iterable.

As a workaround for now, prevent the bug from being any worse by ensuring that these
object members are eagerly typechecked.
  • Loading branch information
bioball committed Oct 30, 2024
1 parent 466ae6f commit c2f7879
Show file tree
Hide file tree
Showing 17 changed files with 192 additions and 32 deletions.
38 changes: 29 additions & 9 deletions pkl-core/src/main/java/org/pkl/core/ast/builder/AstBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@ public ObjectMember visitClazz(ClazzContext ctx) {
headerSection,
isLocal ? VmModifier.LOCAL_CLASS_OBJECT_MEMBER : VmModifier.CLASS_OBJECT_MEMBER,
scope.getName(),
scope.getQualifiedName());
scope.getQualifiedName(),
false);

result.initMemberNode(
new UntypedObjectMemberNode(
Expand Down Expand Up @@ -366,7 +367,8 @@ public ObjectMember visitTypeAlias(TypeAliasContext ctx) {
? VmModifier.LOCAL_TYPEALIAS_OBJECT_MEMBER
: VmModifier.TYPEALIAS_OBJECT_MEMBER,
scopeName,
scope.getQualifiedName());
scope.getQualifiedName(),
false);

result.initMemberNode(
new UntypedObjectMemberNode(
Expand Down Expand Up @@ -697,7 +699,8 @@ private ObjectMember doVisitObjectMethod(
createSourceSection(headerCtx),
modifiers,
scope.getName(),
scope.getQualifiedName());
scope.getQualifiedName(),
false);
var body = visitExpr(exprCtx);
var node =
new ObjectMethodNode(
Expand Down Expand Up @@ -808,7 +811,8 @@ private ObjectMember doVisitObjectProperty(
scope.buildFrameDescriptor(),
modifiers,
bodyNode,
visitTypeAnnotation(typeAnnCtx))
visitTypeAnnotation(typeAnnCtx),
scope.isVisitingIterable())
: VmUtils.createObjectProperty(
language,
sourceSection,
Expand All @@ -818,7 +822,8 @@ private ObjectMember doVisitObjectProperty(
scope.buildFrameDescriptor(),
modifiers,
bodyNode,
null);
null,
scope.isVisitingIterable());
});
}

Expand Down Expand Up @@ -899,8 +904,16 @@ public GeneratorMemberNode visitObjectEntry(ObjectEntryContext ctx) {

@Override
public GeneratorMemberNode visitObjectSpread(ObjectSpreadContext ctx) {
var scope = symbolTable.getCurrentScope();
var visitingIterable = scope.isVisitingIterable();
scope.setVisitingIterable(true);
var expr = visitExpr(ctx.expr());
scope.setVisitingIterable(visitingIterable);
return GeneratorSpreadNodeGen.create(
createSourceSection(ctx), visitExpr(ctx.expr()), ctx.QSPREAD() != null);
createSourceSection(ctx),
expr,
ctx.QSPREAD() != null,
symbolTable.getCurrentScope().isVisitingIterable());
}

private void insertWriteForGeneratorVarsToFrameSlotsNode(@Nullable MemberNode memberNode) {
Expand Down Expand Up @@ -991,7 +1004,11 @@ public GeneratorForNode visitForGenerator(ForGeneratorContext ctx) {
ignoreT1 ? null : visitTypeAnnotation(ctx.t1.typedIdentifier().typeAnnotation());
}

var scope = symbolTable.getCurrentScope();
var visitingIterable = scope.isVisitingIterable();
scope.setVisitingIterable(true);
var iterableNode = visitExpr(ctx.e);
scope.setVisitingIterable(visitingIterable);
var memberNodes = doVisitForWhenBody(ctx.objectBody());
if (keyVariableSlot != -1) {
currentScope.popForGeneratorVariable();
Expand Down Expand Up @@ -1202,7 +1219,8 @@ private ObjectMember doVisitObjectElement(ObjectElementContext ctx) {
elementNode.getSourceSection(),
VmModifier.ELEMENT,
null,
scope.getQualifiedName());
scope.getQualifiedName(),
scope.isVisitingIterable());

if (elementNode instanceof ConstantNode constantNode) {
member.initConstantValue(constantNode);
Expand Down Expand Up @@ -1257,7 +1275,8 @@ private Function<EntryScope, Pair<ExpressionNode, ObjectMember>> objectMemberIns
keyNode.getSourceSection(),
VmModifier.ENTRY,
null,
scope.getQualifiedName());
scope.getQualifiedName(),
scope.isVisitingIterable());

if (valueCtx != null) { // ["key"] = value
var valueNode = visitExpr(valueCtx);
Expand Down Expand Up @@ -1786,7 +1805,8 @@ public ObjectMember visitImportClause(ImportClauseContext ctx) {
importNode.getSourceSection(),
modifiers,
scope.getName(),
scope.getQualifiedName());
scope.getQualifiedName(),
false);

result.initMemberNode(
new UntypedObjectMemberNode(
Expand Down
10 changes: 10 additions & 0 deletions pkl-core/src/main/java/org/pkl/core/ast/builder/SymbolTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ public abstract static class Scope {
private int entryCount = 0;
private final FrameDescriptor.Builder frameDescriptorBuilder;
private final ConstLevel constLevel;
private boolean isVisitingIterable;

private Scope(
@Nullable Scope parent,
Expand All @@ -187,6 +188,7 @@ private Scope(
parent != null && parent.constLevel.biggerOrEquals(constLevel)
? parent.constLevel
: constLevel;
this.isVisitingIterable = parent != null && parent.isVisitingIterable;
}

public final @Nullable Scope getParent() {
Expand Down Expand Up @@ -337,6 +339,14 @@ public final boolean isLexicalScope() {
public ConstLevel getConstLevel() {
return constLevel;
}

public void setVisitingIterable(boolean isVisitingIterable) {
this.isVisitingIterable = isVisitingIterable;
}

public boolean isVisitingIterable() {
return isVisitingIterable;
}
}

private interface LexicalScope {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,17 @@
public abstract class GeneratorSpreadNode extends GeneratorMemberNode {
@Child private ExpressionNode iterableNode;
private final boolean nullable;
private final boolean isInIterable;

public GeneratorSpreadNode(
SourceSection sourceSection, ExpressionNode iterableNode, boolean nullable) {
SourceSection sourceSection,
ExpressionNode iterableNode,
boolean nullable,
boolean isInIterable) {
super(sourceSection);
this.iterableNode = iterableNode;
this.nullable = nullable;
this.isInIterable = isInIterable;
}

protected abstract void executeWithIterable(
Expand Down Expand Up @@ -326,7 +331,8 @@ private ObjectMember createMember(ObjectMember prototype, Object value) {
prototype.getHeaderSection(),
prototype.getModifiers(),
prototype.getNameOrNull(),
prototype.getQualifiedName());
prototype.getQualifiedName(),
isInIterable);
result.initConstantValue(value);
return result;
}
Expand Down
24 changes: 23 additions & 1 deletion pkl-core/src/main/java/org/pkl/core/ast/member/ObjectMember.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import org.pkl.core.util.Nullable;

public final class ObjectMember extends Member {

private final boolean isInIterable;

@CompilationFinal private @Nullable Object constantValue;
@CompilationFinal private @Nullable MemberNode memberNode;

Expand All @@ -35,9 +38,11 @@ public ObjectMember(
SourceSection headerSection,
int modifiers,
@Nullable Identifier name,
String qualifiedName) {
String qualifiedName,
boolean isInIterable) {

super(sourceSection, headerSection, modifiers, name, qualifiedName);
this.isInIterable = isInIterable;
}

public void initConstantValue(ConstantNode node) {
Expand Down Expand Up @@ -152,4 +157,21 @@ public boolean equals(@Nullable Object obj) {
public int hashCode() {
return System.identityHashCode(this);
}

/**
* Tells if this member is declared inside the iterable of a for-generator, or an object spread.
*
* This is <p>{@code true} for {@code new {}} within:
*
* <pre>
* {@code
* for (x in new Listing { new {} }) {
* ^^^^^^
* // etc
* }
* </pre>
*/
public boolean isInIterable() {
return isInIterable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,30 @@ public String getName() {
return qualifiedPropertyName;
}

private boolean isInIterable(VirtualFrame frame) {
if (frame.getArguments().length < 4) {
return false;
}
if (frame.getArguments()[3] instanceof Boolean isInIterable) {
return isInIterable;
}
return false;
}

@Override
public Object execute(VirtualFrame frame) {
try {
if (isInIterable(frame)) {
// There is currently a bug around resolving variables within the iterable of a for
// generator or spread syntax (https://github.com/apple/pkl/issues/741)
//
// Normally, mappings/listings are type-checked lazily. However, this results in the said
// bug getting widened, for any object members declared in the iterable.
//
// As a workaround for now, prevent the bug from being any worse by ensuring that these
// object members are eagerly typechecked.
return typeNode.executeEagerly(frame, frame.getArguments()[2]);
}
return typeNode.execute(frame, frame.getArguments()[2]);
} catch (VmTypeMismatchException e) {
CompilerDirectives.transferToInterpreter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
public abstract class TypeCheckedPropertyNode extends RegularMemberNode {
@Child @Executed protected ExpressionNode ownerNode = new GetOwnerNode();

private final ObjectMember member;

protected TypeCheckedPropertyNode(
@Nullable VmLanguage language,
FrameDescriptor descriptor,
Expand All @@ -40,6 +42,7 @@ protected TypeCheckedPropertyNode(
super(language, descriptor, member, bodyNode);

assert member.isProp();
this.member = member;
}

@SuppressWarnings("unused")
Expand All @@ -55,7 +58,8 @@ protected Object evalTypedObjectCached(

// TODO: propagate SUPER_CALL_MARKER to disable constraint (but not type) check
if (callNode != null && VmUtils.shouldRunTypeCheck(frame)) {
return callNode.call(VmUtils.getReceiverOrNull(frame), property.getOwner(), result);
return callNode.call(
VmUtils.getReceiverOrNull(frame), property.getOwner(), result, member.isInIterable());
}

return result;
Expand All @@ -75,7 +79,8 @@ protected Object eval(
typeAnnNode.getCallTarget(),
VmUtils.getReceiverOrNull(frame),
property.getOwner(),
result);
result,
member.isInIterable());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
/** A property definition that has a type annotation. */
public final class TypedPropertyNode extends RegularMemberNode {
@Child private DirectCallNode typeCheckCallNode;
private final ObjectMember member;

@TruffleBoundary
public TypedPropertyNode(
Expand All @@ -38,6 +39,7 @@ public TypedPropertyNode(
super(language, descriptor, member, bodyNode);

assert member.isProp();
this.member = member;

typeCheckCallNode = DirectCallNode.create(typeNode.getCallTarget());
}
Expand All @@ -47,7 +49,10 @@ public Object execute(VirtualFrame frame) {
var propertyValue = executeBody(frame);
if (VmUtils.shouldRunTypeCheck(frame)) {
return typeCheckCallNode.call(
VmUtils.getReceiver(frame), VmUtils.getOwner(frame), propertyValue);
VmUtils.getReceiver(frame),
VmUtils.getOwner(frame),
propertyValue,
member.isInIterable());
}
return propertyValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ public ClassProperty execute(VirtualFrame frame, VmClass clazz) {
descriptor,
modifiers,
effectiveBodyNode,
typeNode);
typeNode,
false);

return new ClassProperty(
sourceSection,
Expand Down
3 changes: 2 additions & 1 deletion pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -1571,7 +1571,8 @@ public final Object createDefaultValue(
headerSection,
VmModifier.HIDDEN,
Identifier.DEFAULT,
qualifiedName + ".default");
qualifiedName + ".default",
false);

var defaultMemberValue =
valueTypeNode.createDefaultValue(language, headerSection, qualifiedName);
Expand Down
3 changes: 2 additions & 1 deletion pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,8 @@ private EconomicMap<Object, ObjectMember> createDelegatingMembers(
VmUtils.unavailableSourceSection(),
VmModifier.NONE,
name,
name.toString());
name.toString(),
false);
member.initMemberNode(memberNodeFactory.apply(member));
result.put(name, member);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ public VmObjectBuilder addEntry(Object key, Object value) {
public VmObjectBuilder addEntry(Object key, SharedMemberNode valueNode) {
var entry =
new ObjectMember(
valueNode.getSourceSection(), valueNode.getHeaderSection(), VmModifier.ENTRY, null, "");
valueNode.getSourceSection(),
valueNode.getHeaderSection(),
VmModifier.ENTRY,
null,
"",
false);
entry.initMemberNode(valueNode);
EconomicMaps.put(members, key, entry);
return this;
Expand Down
Loading

0 comments on commit c2f7879

Please sign in to comment.