Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix globbed read/import bugs #449

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

translatenix
Copy link
Contributor

@translatenix translatenix commented Apr 24, 2024

This PR fixes several bugs related to globbed reads/imports and makes code improvements along the way.

Bug fixes:

  • Fix caching of globbed reads
    • Glob pattern isn't guaranteed to be a constant
    • Both glob resolution and ResolvedGlobElement.getPath() depend on the current module URI
  • Fix ClassCastException when reflecting on globbed import
  • Avoid creating an unbounded number of Truffle root nodes per globbed read/import

See commit messages for further details.

SharedMemberNode enables generating non-constant object members
at run time without generating an unbounded number of Truffle root nodes.
Introduce VmObjectBuilder, a uniform way to build `VmObject`s
whose `ObjectMember`s are determined at run time.
Replace some manual object building code with VmObjectBuilder.
Add some assertions to fix IntelliJ warnings.
- Leverage SharedMemberNode to have a single Truffle root node per globbed
  read/import expression instead of one root node per resolved glob element.
- Remove caching in ReadGlobNode because it only works correctly if glob pattern is a string *constant*.
- Remove caching in StaticReadNode (now ReadGlobElementNode/ImportGlobElementNode)
  because it seems unnecessary and the implementation doesn't look quite right.
Problem:
The result of a globbed read depends not only on the glob pattern but also on the current module URI.
However, ResourceManager does not take this into account when caching globbed reads, causing wrong results.

Changes:
- Cache globbed reads per read expression.
  This is also how globbed imports are cached, except that import URIs are constant.
- Make ReadGlobNode and ImportGlobNode code as similar as possible.
- Reduce code duplication by inheriting from AbstractReadNode.
- Add some tests.
Copy link
Contributor

@bioball bioball left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, great work!!

I have some minor blocking comments, but overall this is a great improvement. Thanks so much for your PR.

currentModule.getOriginal(),
currentModule.getUri(),
globPattern);
var builder = new VmObjectBuilder();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var builder = new VmObjectBuilder();
var builder = new VmObjectBuilder(resolvedElements.size());

private final String globPattern;
private final SharedMemberNode importGlobElementNode;
Copy link
Contributor

@bioball bioball May 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should declare this @Child. Also, it's good to avoid allocating an extra node until it's needed.

Suggested change
private final SharedMemberNode importGlobElementNode;
@Child @LateInit private SharedMemberNode importGlobElementNode;

Add:

  private SharedMemberNode getMemberNode() {
    if (importGlobElementNode == null) {
      CompilerDirectives.transferToInterpreterAndInvalidate();
      var language = VmLanguage.get(this);
      importGlobElementNode = new SharedMemberNode(
        sourceSection,
        sourceSection,
        "",
        language,
        new FrameDescriptor(),
        new ImportGlobElementNode(sourceSection, language, currentModule));
    }
    return importGlobElementNode;
  }

Then within executeGeneric:

-builder.addEntry(entry.getKey(), importGlobElementNode);
+builder.addEntry(entry.getKey(), getMemberNode());

Same comment applies to ReadGlobNode.

import org.pkl.core.util.GlobResolver.ResolvedGlobElement;

/** Used by {@link ReadGlobNode}. */
public class ReadGlobElementNode extends ExpressionNode {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Element" clashes with our element/entry/property lexicon.

Maybe:

Suggested change
public class ReadGlobElementNode extends ExpressionNode {
public class ReadGlobMemberBodyNode extends ExpressionNode {

Same comment on the import side; let's rename ImportGlobElementNode to maybe ImportGlobMemberBodyNode

@TruffleBoundary
private Object readResource(VmObjectLike mapping, String path) {
@SuppressWarnings("unchecked")
var globElements = (Map<String, ResolvedGlobElement>) mapping.getExtraStorage();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, great idea! I didn't think about this, but this is much better. And this mirrors how we do this in the for generators too.

Comment on lines +41 to +50
@TruffleBoundary
private Object readResource(VmObjectLike mapping, String path) {
@SuppressWarnings("unchecked")
var globElements = (Map<String, ResolvedGlobElement>) mapping.getExtraStorage();
var resourceUri = globElements.get(path).getUri();
var resource = VmContext.get(this).getResourceManager().read(resourceUri, this).orElse(null);
if (resource == null) {
throw exceptionBuilder().evalError("cannotFindResource", resourceUri).build();
}
return resource;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get rid of this truffle boundary:

Suggested change
@TruffleBoundary
private Object readResource(VmObjectLike mapping, String path) {
@SuppressWarnings("unchecked")
var globElements = (Map<String, ResolvedGlobElement>) mapping.getExtraStorage();
var resourceUri = globElements.get(path).getUri();
var resource = VmContext.get(this).getResourceManager().read(resourceUri, this).orElse(null);
if (resource == null) {
throw exceptionBuilder().evalError("cannotFindResource", resourceUri).build();
}
return resource;
private Object readResource(VmObjectLike mapping, String path) {
@SuppressWarnings("unchecked")
var globElements = (Map<String, ResolvedGlobElement>) mapping.getExtraStorage();
var resourceUri = VmUtils.getMapValue(globElements, path).getUri();
var resource = VmContext.get(this).getResourceManager().read(resourceUri, this).orElse(null);
if (resource == null) {
CompilerDirectives.transferToInterpreter();
throw exceptionBuilder().evalError("cannotFindResource", resourceUri).build();
}
return resource;

This change will require VmUtils.getMapValue(), which should be as simple as:

  @TruffleBoundary
  public static <K, V> V getMapValue(Map<K, V> map, K key) {
    return map.get(key);
  }

currentModule,
currentModule.getUri(),
globPattern);
var builder = new VmObjectBuilder();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var builder = new VmObjectBuilder();
var builder = new VmObjectBuilder(resolvedElements.size());

/**
* Resolves the glob URI into a set of URIs.
*
* <p>The glob URI must be absolute. For example: {@code "file:///foo/bar/*.pkl"}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is an artifact of how globbing was first implemented (glob patterns expanded to their absolute path). Moving away from that yields the bug that you noticed. We should have added an assertion here.

Comment on lines +228 to +253
@TruffleBoundary
public VmMapping toMapping() {
var builder = new VmObjectBuilder(keyOrder.size());
for (var key : keyOrder) {
var value = map.get(key);
assert value != null;
builder.addEntry(key, value);
}
return builder.toMapping();
}

@TruffleBoundary
public VmDynamic toDynamic() {
var builder = new VmObjectBuilder(keyOrder.size());
for (var key : keyOrder) {
var value = map.get(key);
assert value != null;
if (key instanceof String) {
builder.addProperty(Identifier.get((String) key), value);
} else {
builder.addEntry(key, value);
}
}
return builder.toDynamic();
}

Copy link
Contributor

@bioball bioball May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a replacement for code that was visible to Truffle inlining, so it shouldn't be then moved behind a TruffleBoundary.

Here is an implementation of these two methods in a way that can be inlined:

Suggested change
@TruffleBoundary
public VmMapping toMapping() {
var builder = new VmObjectBuilder(keyOrder.size());
for (var key : keyOrder) {
var value = map.get(key);
assert value != null;
builder.addEntry(key, value);
}
return builder.toMapping();
}
@TruffleBoundary
public VmDynamic toDynamic() {
var builder = new VmObjectBuilder(keyOrder.size());
for (var key : keyOrder) {
var value = map.get(key);
assert value != null;
if (key instanceof String) {
builder.addProperty(Identifier.get((String) key), value);
} else {
builder.addEntry(key, value);
}
}
return builder.toDynamic();
}
public VmMapping toMapping() {
var builder = new VmObjectBuilder(getLength());
for (var entry : this) {
builder.addEntry(VmUtils.getKey(entry), VmUtils.getValue(entry));
}
return builder.toMapping();
}
public VmDynamic toDynamic() {
var builder = new VmObjectBuilder(getLength());
for (var entry : this) {
var key = VmUtils.getKey(entry);
var value = VmUtils.getValue(entry);
if (key instanceof String) {
builder.addProperty(Identifier.get((String) key), value);
} else {
builder.addEntry(key, value);
}
}
return builder.toDynamic();
}

VmUtils.createEmptyMaterializedFrame(),
BaseModule.getMappingClass().getPrototype(),
members);
return self.toMapping();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should change class toDynamic to also call out to self.toDynamic() yeah?

@@ -463,15 +465,15 @@ private static Pair<String, String[]> splitGlobPatternIntoBaseAndWildcards(
* <p>Each pair is the expanded form of the glob pattern, paired with its resolved absolute URI.
*/
@TruffleBoundary
public static List<ResolvedGlobElement> resolveGlob(
public static Map<String, ResolvedGlobElement> resolveGlob(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this change from List to Map?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants