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

Extract mutable builder from ModuleScope #9914

Merged
merged 27 commits into from
Jun 5, 2024

Conversation

hubertp
Copy link
Contributor

@hubertp hubertp commented May 10, 2024

Pull Request Description

Refactored mutable parts of ModuleScope into builder to make it easier to reduce unnecessary locks.

Important Notes

Elements of ModuleScope (types, imports etc) are used while building of it may still be in progress. In order to make static typing happy, every ModuleScope.Builder can be exposed as (unmodifiable) ModuleScope.

Checklist

Please ensure that the following checklist has been satisfied before submitting the PR:

  • The documentation has been updated, if necessary.
  • Screenshots/screencasts have been attached, if there are any visual changes. For interactive or animated visual changes, a screencast is preferred.
  • All code follows the
    Scala,
    Java,
    TypeScript,
    and
    Rust
    style guides. In case you are using a language not listed above, follow the Rust style guide.
  • Unit tests have been written where possible.

@hubertp hubertp added the CI: No changelog needed Do not require a changelog entry for this PR. label May 13, 2024
Copy link
Member

@JaroslavTulach JaroslavTulach left a comment

Choose a reason for hiding this comment

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

Ideally, I'd like the Builder to be more hidden, not flying around being accessed by everybody. But even the current state that makes ModuleScope immutable is a step forward and deserves to be integrated. Thank you!

private Map<Type, Map<Type, Function>> conversions;
private Set<ModuleScope> imports;
private Set<ModuleScope> exports;
private final Map<String, Object> polyglotSymbols;
Copy link
Member

Choose a reason for hiding this comment

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

Yup, this is great. Having all fields in ModuleScope final, is the goal.


public ModuleScope build() {
if (moduleScope == null) {
synchronized (this) {
Copy link
Member

Choose a reason for hiding this comment

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

synchronized? Only useful if we encourage the Builder to be used from multiple threads. But it shouldn't be used that way. A Builder should only be used from a single thread, am I right?

If so, we should assert Thread.currentThread() == builderCreatorThread at few places to ensure Builder is always used from a single thread.

Copy link
Member

Choose a reason for hiding this comment

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

AFAIK, ModuleScope is constructed only from IrToTruffle and that is single-threaded. Moreover, only one module is processed at single time. So I believe that usage of synchronized does not make sense as well. Moreover, I believe that it does not make sense to use ConcurrentHashMap as fields.

Copy link
Member

Choose a reason for hiding this comment

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

Note that I have removed all the ConcurrentHashMap stuff in my recently merged PR.

Copy link
Contributor Author

@hubertp hubertp May 24, 2024

Choose a reason for hiding this comment

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

If we enable multi-threading for visualizations, which can as a side-effect trigger compilation, then we can have multiple threads accessing/adding to the builder. That was also the motivation for this PR, to make it more explicit.

@@ -131,7 +131,7 @@ import scala.jdk.OptionConverters._
class IrToTruffle(
val context: EnsoContext,
val source: Source,
val moduleScope: ModuleScope,
val moduleScope: ModuleScope.Builder,
Copy link
Member

Choose a reason for hiding this comment

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

That looks promising!

Copy link
Member

@Akirathan Akirathan left a comment

Choose a reason for hiding this comment

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

Altough draft PR, I am still submitting "Request changes" here. In the current form, I don't see a lot of improvements over the previous state. I would like to see most, if not all, the runtime nodes and classes to receive immutable ModuleScope as parameters and fields, not ModuleScope.Builder.


public ModuleScope build() {
if (moduleScope == null) {
synchronized (this) {
Copy link
Member

Choose a reason for hiding this comment

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

AFAIK, ModuleScope is constructed only from IrToTruffle and that is single-threaded. Moreover, only one module is processed at single time. So I believe that usage of synchronized does not make sense as well. Moreover, I believe that it does not make sense to use ConcurrentHashMap as fields.

@@ -54,7 +54,7 @@ public class ClosureRootNode extends EnsoRootNode {
public static ClosureRootNode build(
EnsoLanguage language,
LocalScope localScope,
ModuleScope moduleScope,
ModuleScope.Builder moduleScope,
Copy link
Member

Choose a reason for hiding this comment

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

I believe only IrToTruffle should use ModuleScope.Builder for its purposes. All the runtime nodes should have only immutable ModuleScope. Is that possible? Having ModuleScope.Builder as fields in some truffle nodes diminishes the whole idea of this PR, since we will still be able to mutate the underlying builder at runtime, which should be restricted. No?

private final String name;
private @CompilerDirectives.CompilationFinal ModuleScope definitionScope;
private @CompilerDirectives.CompilationFinal ModuleScope.Builder definitionScope;
Copy link
Member

Choose a reason for hiding this comment

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

DOes it make sense to declare fields of data as @CompilationFinal?

Copy link
Member

Choose a reason for hiding this comment

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

It might make sense. Instances of Type are directly referenced from various Enso Node - e.g. they are part of the AST. Remember we have the EXCLUSIVE sharing policy - there is no sharing of ASTs.

Copy link
Member

Choose a reason for hiding this comment

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

However, please don't reference builder, reference Module.

@hubertp
Copy link
Contributor Author

hubertp commented May 24, 2024

@Akirathan @JaroslavTulach
To cut the long story short, your requirement of having only ModuleScope is satisfied in runtime because you won't be able to mutate builder once something calls build(). Statically, yes, you still see ModuleScope.Builder in various places.

I understand the sentiment that builder shouldn't be visible outside of IrToTruffle but take into account that since Truffle nodes are built during IrToTruffle the only thing that I can provide reference to is ModuleScope.Builder not ModuleScope.
In fact that is pretty much the only reason why you still see ModuleScope.Builder in various nodes.
I wanted to have only ModuleScope in Truffle nodes but I have yet to figure out, if possible at all, how to make typechecker happy with that requirement.

Added `built` method, apart from the existing `build` method, to return
ModuleScope associated with the builder.
This way it becomes clear when builder transforms into `ModuleScope`.
@JaroslavTulach
Copy link
Member

builder shouldn't be visible outside of IrToTruffle but take into account that since Truffle nodes are built during IrToTruffle the only thing that I can provide reference to is ModuleScope.Builder not ModuleScope.

Don't we have to support some form of mutability to allow recompilation of Module? Where such mutability happens? A Module may get assigned different ModuleScope? If so...

I can provide reference to is ModuleScope.Builder not ModuleScope.

... provide reference to uninitialized Module and fill it in later when .build() is called.

`ImportExportScope` serves as a proxy to the underlying module's scope.
Can't reference the scope directly at the point when it is built because
building of some modules might still be in progress.

This also allowed to get rid off the horrible `withTypes` method that
would create a temporary module scope.
It's unused and untested anyway.
Simplified the whole approach by avoiding calls to `built` and `build`
to determine the state of `ModuleScope.Builder`.
Instead, `ModuleScope.Builder` is only passed whenever a registration is
necessary. Since builder is accessed in various places, while it may
still, in theory, be constructor, this change introduces a simple,
short-lived, proxy that delegates to accessors and satisfies the
`ModuleScope` API.
@hubertp hubertp force-pushed the wip/hubert/9736-visualizations branch from e4e2f20 to 1f347d0 Compare June 2, 2024 20:27
@hubertp hubertp marked this pull request as ready for review June 4, 2024 10:44
@hubertp hubertp requested a review from Akirathan June 4, 2024 10:44
@hubertp hubertp changed the title WIP: Extract mutable builder from ModuleScope Extract mutable builder from ModuleScope Jun 4, 2024
Copy link
Member

@JaroslavTulach JaroslavTulach left a comment

Choose a reason for hiding this comment

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

I don't think the PR gets us to a state where I'd like to be, but it is certainly making the system better. Ideally I want:

var b = module.newScopeBuilder(reuseYesOrNo);
b.register(...)
b.build() // andInstall();

where buildAndInstall() actually verifies the current module scope is the same as the scope when the builder was created and otherwise yields an inconsistency error.

@@ -348,7 +340,7 @@ get_polyglot_language value = @Builtin_Method "Meta.get_polyglot_language"

Arguments:
- name: The name of the unresolved symbol.
- scope: The scope in which the symbol name is unresolved.
- symbol: The unresolved symbol from which to get the scope.
create_unresolved_symbol : Text -> Any -> Unresolved_Symbol
create_unresolved_symbol name scope = @Builtin_Method "Meta.create_unresolved_symbol"
Copy link
Member

Choose a reason for hiding this comment

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

The argument is still named scope.

} else {
return scope.withTypes(types);
}
public ModuleScope.Builder getScopeBuilder() {
Copy link
Member

Choose a reason for hiding this comment

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

Exposing current builder (created by whoever else), isn't really safe.

return scopeBuilder;
}

public ModuleScope.Builder newScopeBuilder() {
Copy link
Member

Choose a reason for hiding this comment

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

Creating new builder is OK. But storing it in this.scopeBuilder variable isn't correct.

return builder;
}

public void resetScope() {
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer newScopeBuilder(boolean inheritTypes) slightly...

private ModuleSources sources;
private QualifiedName name;
Copy link
Member

Choose a reason for hiding this comment

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

Why isn't QualifiedName final? I don't see any write to it...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Three is a rename method.

Copy link
Member

Choose a reason for hiding this comment

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

I see. Thank you. However, in the name of exorcising mutability away from Module it may be reasonable to ask whether name shouldn't be a temporary property - e.g. be held by ModuleScope?

private final String name;
private @CompilerDirectives.CompilationFinal ModuleScope definitionScope;
private @CompilerDirectives.CompilationFinal ModuleScope.Builder definitionScope;
Copy link
Member

Choose a reason for hiding this comment

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

However, please don't reference builder, reference Module.

* A proxy scope delegating to the underlying module's scope. Additionally, `ImportExportScope` may
* limit the number of types that are imported/exported.
*/
public class ImportExportScope implements EnsoObject {
Copy link
Member

Choose a reason for hiding this comment

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

Why don't this object extend EnsoObject? It shouldn't flow thru user data right?

Make final.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

? EnsoObject is an interface, I don't understand the request.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, EnsoObject is an interface with a purpose (described in its javadoc) of:

/** All non-primitive Enso types extends from {@code EnsoObject}. */

why do you believe ImportExportScope should be a non-primitive Enso type? Do you believe this object shall flow thru user program? Unless I am mistaken you have removed exposure of ModuleScope from Meta.enso claiming "it is dangerous" - why do you believe ImportExportScope should be exposed (and manipulated) by Enso programs?

Suggested change
public class ImportExportScope implements EnsoObject {
public final class ImportExportScope {


import org.enso.compiler.context.CompilerContext;

public class TruffleCompilerModuleScopeBuilder extends CompilerContext.ModuleScopeBuilder {
Copy link
Member

Choose a reason for hiding this comment

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

final

@@ -723,8 +734,7 @@ public void close() {
module.module.setLoadedFromCache(loadedFromCache);
}
if (resetScope) {
module.module.ensureScopeExists();
module.module.getScope().reset();
module.module.resetScope();
Copy link
Member

Choose a reason for hiding this comment

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

I would expect this to call newBuilder().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

resetScope does call newBuilder

@@ -86,7 +86,7 @@ RootCallTarget parseExpression(LocalScope scope, ModuleScope moduleScope, String
var sco = newInlineContext.localScope().getOrElse(LocalScope::root);
var mod = newInlineContext.getModule();
var m = org.enso.interpreter.runtime.Module.fromCompilerModule(mod);
var toTruffle = new IrToTruffle(context, src, m.getScope(), compiler.getConfig());
var toTruffle = new IrToTruffle(context, src, m.getScopeBuilder(), compiler.getConfig());
Copy link
Member

Choose a reason for hiding this comment

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

I can imagine that this is a place where EvalNode wants to incrementally add something to the scope. Still having some sequence like:

var b = m.newScopeBuilder(reuseEverything=true)
b.registerXyz(...)
b.build() // and install into `Module`

is a way better than having a getter to existing scope builder.

Copy link
Member

@Akirathan Akirathan left a comment

Choose a reason for hiding this comment

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

Generally looks like a better design than the one we currently have. There might be some follow-up issues.

@hubertp hubertp added the CI: Clean build required CI runners will be cleaned before and after this PR is built. label Jun 5, 2024

public static class Builder {

@CompilerDirectives.CompilationFinal private ModuleScope moduleScope = null;
Copy link
Member

Choose a reason for hiding this comment

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

I doubt this is correct. How is a code using this moduleScope field in PE going to be invalidated when the field changes?

Anyway, I believe Builder shouldn't be used on fast path. Rather put CompilerAsserts.neverPartOfCompilation(); to every method of the Builder!

@hubertp
Copy link
Contributor Author

hubertp commented Jun 5, 2024

There are few minor issues that I'd like to address separately, as this PR has already been dragging on. Most importantly remove the getScopeBuilder method and instead pass it around during compilation. But I will make the change in a follow up.

@hubertp hubertp added CI: Ready to merge This PR is eligible for automatic merge and removed CI: Ready to merge This PR is eligible for automatic merge labels Jun 5, 2024
@hubertp hubertp added the CI: Ready to merge This PR is eligible for automatic merge label Jun 5, 2024
@mergify mergify bot merged commit 1bc1425 into develop Jun 5, 2024
36 checks passed
@mergify mergify bot deleted the wip/hubert/9736-visualizations branch June 5, 2024 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI: Clean build required CI runners will be cleaned before and after this PR is built. CI: No changelog needed Do not require a changelog entry for this PR. CI: Ready to merge This PR is eligible for automatic merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants