Skip to content

Commit

Permalink
Reimplement enso_project as a proper builtin (#6352)
Browse files Browse the repository at this point in the history
Remove the magical code generation of `enso_project` method from codegen phase and reimplement it as a proper builtin method.

The old behavior of `enso_project` was special, and violated the language semantics (regarding the `self` argument):
- It was implicitly declared in every module, so it could be called without a self argument.
- It can be called with explicit module as self argument, e.g. `Base.enso_project`, or `Visualizations.enso_project`.

Let's avoid implicit methods on modules and let's be explicit. Let's reimplement the `enso_project` as a builtin method. To comply with the language semantics, we will have to change the signature a bit:
- `enso_project` is a static method in the `Standard.Base.Meta.Enso_Project` module.
- It takes an optional `project` argument (instead of taking it as an explicit self argument).

Having the `enso_project` defined as a (shadowed) builtin method, we will automatically have suggestions created for it.

# Important Notes
- Truffle nodes are no longer generated in codegen phase for the `enso_project` method. It is a standard builtin now.
- The minimal import to use `enso_project` is now `from Standard.Base.Meta.Enso_Project import enso_project`.
- Tested implicitly by `org.enso.compiler.ExecCompilerTest#testInvalidEnsoProjectRef`.
  • Loading branch information
Akirathan authored May 2, 2023
1 parent 6dee755 commit 3a42d0c
Show file tree
Hide file tree
Showing 21 changed files with 277 additions and 165 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,7 @@
- [Removing need for asynchronous thread to execute ResourceManager
finalizers][6335]
- [Warning.get_all returns only unique warnings][6372]
- [Reimplement `enso_project` as a proper builtin][6352]

[3227]: https://github.com/enso-org/enso/pull/3227
[3248]: https://github.com/enso-org/enso/pull/3248
Expand Down Expand Up @@ -827,6 +828,7 @@
[6171]: https://github.com/enso-org/enso/pull/6171
[6335]: https://github.com/enso-org/enso/pull/6335
[6372]: https://github.com/enso-org/enso/pull/6372
[6352]: https://github.com/enso-org/enso/pull/6352

# Enso 2.0.0-alpha.18 (2021-10-12)

Expand Down
63 changes: 6 additions & 57 deletions app/gui/src/controller/searcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use crate::model::module::NodeMetadata;
use crate::model::suggestion_database;

use breadcrumbs::Breadcrumbs;
use const_format::concatcp;
use double_representation::graph::GraphInfo;
use double_representation::graph::LocationHint;
use double_representation::import;
Expand Down Expand Up @@ -50,12 +49,6 @@ pub use action::Action;
/// See: https://github.com/enso-org/ide/issues/1067
pub const ASSIGN_NAMES_FOR_NODES: bool = true;

/// The special module used for mock `Enso_Project.data` entry.
/// See also [`Searcher::add_enso_project_entries`].
const ENSO_PROJECT_SPECIAL_MODULE: &str =
concatcp!(project::STANDARD_BASE_LIBRARY_PATH, ".Enso_Project");



// ==============
// === Errors ===
Expand Down Expand Up @@ -862,13 +855,7 @@ impl Searcher {
})
.flatten();
let mut module = self.module();
// TODO[ao] this is a temporary workaround. See [`Searcher::add_enso_project_entries`]
// documentation.
let enso_project_special_import = suggestion_database::entry::Import::Qualified {
module: ENSO_PROJECT_SPECIAL_MODULE.try_into().unwrap(),
};
let without_enso_project = imports.filter(|i| *i != enso_project_special_import);
for entry_import in without_enso_project {
for entry_import in imports {
let already_imported =
module.iter_imports().any(|existing| entry_import.covered_by(&existing));
let import: import::Info = entry_import.into();
Expand Down Expand Up @@ -1029,9 +1016,6 @@ impl Searcher {
}
let libraries_cat =
libraries_root_cat.add_category("Libraries", libraries_icon.clone_ref());
if should_add_additional_entries {
Self::add_enso_project_entries(&libraries_cat);
}
let entries = completion_response.results.iter().filter_map(|id| {
self.database
.lookup(*id)
Expand Down Expand Up @@ -1093,34 +1077,6 @@ impl Searcher {
fn module_qualified_name(&self) -> QualifiedName {
self.graph.module_qualified_name(&*self.project)
}

/// Add to the action list the special mocked entry of `Enso_Project.data`.
///
/// This is a workaround for Engine bug https://github.com/enso-org/enso/issues/1605.
//TODO[ao] this is a temporary workaround.
fn add_enso_project_entries(libraries_cat_builder: &action::CategoryBuilder) {
// We may unwrap here, because the constant is tested to be convertible to
// [`QualifiedName`].
let module = QualifiedName::from_text(ENSO_PROJECT_SPECIAL_MODULE).unwrap();
let self_type = module.clone();
for method in &["data", "root"] {
let entry = model::suggestion_database::Entry {
name: (*method).to_owned(),
kind: model::suggestion_database::entry::Kind::Method,
defined_in: module.clone(),
arguments: vec![],
return_type: "Standard.Base.System.File.File".try_into().unwrap(),
documentation: vec![],
self_type: Some(self_type.clone()),
is_static: true,
scope: model::suggestion_database::entry::Scope::Everywhere,
icon_name: None,
reexported_in: None,
};
let action = Action::Suggestion(action::Suggestion::FromDatabase(Rc::new(entry)));
libraries_cat_builder.add_action(action);
}
}
}


Expand Down Expand Up @@ -1346,13 +1302,6 @@ pub mod test {
use parser::Parser;
use std::assert_matches::assert_matches;


#[test]
fn enso_project_special_module_is_convertible_to_qualified_names() {
QualifiedName::from_text(ENSO_PROJECT_SPECIAL_MODULE)
.expect("ENSO_PROJECT_SPECIAL_MODULE should be convertible to QualifiedName.");
}

pub fn completion_response(results: &[SuggestionId]) -> language_server::response::Completion {
language_server::response::Completion {
results: results.to_vec(),
Expand Down Expand Up @@ -1682,11 +1631,11 @@ pub mod test {
assert!(searcher.actions().is_loading());
fixture.test.run_until_stalled();
let list = searcher.actions().list().unwrap().to_action_vec();
// There are 8 entries, because: 2 were returned from `completion` method, two are mocked,
// and all of these are repeated in "All Search Result" category.
assert_eq!(list.len(), 8);
assert_eq!(list[2], Action::Suggestion(action::Suggestion::FromDatabase(test_function_1)));
assert_eq!(list[3], Action::Suggestion(action::Suggestion::FromDatabase(test_function_2)));
// There are 4 entries, because: 2 were returned from `completion` method, and two are
// mocked.
assert_eq!(list.len(), 4);
assert_eq!(list[0], Action::Suggestion(action::Suggestion::FromDatabase(test_function_1)));
assert_eq!(list[1], Action::Suggestion(action::Suggestion::FromDatabase(test_function_2)));
let notification = subscriber.next().boxed_local().expect_ready();
assert_eq!(notification, Some(Notification::NewActionList));
}
Expand Down
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,7 @@ lazy val `polyglot-api` = project

lazy val `language-server` = (project in file("engine/language-server"))
.settings(
commands += WithDebugCommand.withDebug,
frgaalJavaCompilerSetting,
libraryDependencies ++= akka ++ circe ++ Seq(
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
Expand Down
2 changes: 2 additions & 0 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import project.Function
import project.IO
import project.Math
import project.Meta
from project.Meta.Enso_Project import enso_project
import project.Nothing.Nothing
import project.Panic.Panic
import project.Polyglot.Java
Expand Down Expand Up @@ -42,6 +43,7 @@ export project.Errors
export project.IO
export project.Math
export project.Meta
from project.Meta.Enso_Project export enso_project
export project.Nothing.Nothing
export project.Panic.Panic
export project.Polyglot.Java
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import project.Any.Any
import project.Data.Text.Text
import project.Nothing.Nothing
import project.System.File.File

from project.Errors.Common import Module_Not_In_Package_Error

## Functionality for inspecting the current project.
@Builtin_Type
type Project_Description
Expand Down Expand Up @@ -47,3 +51,34 @@ type Project_Description
enso_project.namespace
namespace : Text
namespace self = self.prim_config.namespace

## Returns the Enso project description for the given module. If no module is
given, returns the description of the project that the engine was executed
with, i.e., the project that contains the `main` method, or throws
`Module_Not_In_Package_Error` if there is no such project, e.g., when
executing a single file.

Arguments:
- module: An optional module for which the Enso project description
will be fetched. If `Nothing`, the current project description will
be fetched.

> Example
Get the project description for the project that contains the `main`
method.

enso_project

> Example
Get the project description for the `Standard.Visualizations` project.

enso_project Standard.Visualizations

> Example
Get the project description for the `Standard.Base` project from the
`Vector` module.

enso_project Standard.Base.Data.Vector
enso_project : (Any | Nothing) -> Project_Description ! Module_Not_In_Package_Error
enso_project module=Nothing =
Project_Description.enso_project_builtin module
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,4 @@ type Id

example_id = Visualization.Id.from_module Base "My Visualization"
from_module module visualization_name =
Id.Library module.enso_project visualization_name
Id.Library (enso_project module) visualization_name
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package org.enso.interpreter.node.expression.builtin.meta;

import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
import java.lang.ref.WeakReference;
import java.util.Optional;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.EnsoRootNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.atom.Atom;
import org.enso.interpreter.runtime.data.EnsoFile;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.library.dispatch.TypesLibrary;
import org.enso.pkg.Package;

@BuiltinMethod(
type = "Project_Description",
name = "enso_project_builtin",
description = "Returns the project description of the project given as the argument")
public abstract class EnsoProjectNode extends Node {

public static EnsoProjectNode build() {
return EnsoProjectNodeGen.create();
}

/**
* It is OK to cache EnsoContext and project description atom here, since this node should be
* cloned into every separate caller's AST.
*/
@CompilationFinal
private WeakReference<EnsoContext> previousCtxRef = new WeakReference<>(null);

@CompilationFinal
private WeakReference<Object> cachedProjectDescrRef = new WeakReference<>(null);

/**
* @param module Either {@code Nothing}, or a module.
*/
public abstract Object execute(Object module);

/**
* Fetches the second stack frame from Truffle runtime (note that the first stack frame is the
* call of {@code enso_project}) - the caller of {@code enso_project}, and finds in which package
* the caller is located.
*
* @param nothing Nothing, or interop null.
*/
@Specialization(guards = "isNothing(nothing)")
public Object getCurrentProjectDescr(Object nothing) {
var ctx = EnsoContext.get(this);
var previousCtx = previousCtxRef.get();
var cachedProjectDescr = cachedProjectDescrRef.get();
if (previousCtx == null || cachedProjectDescr == null || previousCtx != ctx) {
CompilerDirectives.transferToInterpreter();
previousCtxRef = new WeakReference<>(ctx);
// Find the caller of `enso_project`, i.e., of this node, and find in which package
// it is located. The first frame is skipped, because it is always
// `Enso_Project.enso_project`,
// i.e., the first frame is always call of this specialization.
Optional<Package<TruffleFile>> pkgOpt =
Truffle.getRuntime()
.iterateFrames(
frame -> {
var callNode = frame.getCallNode();
assert callNode != null
: "Should skip the first frame, therefore, callNode should not be null";
var callRootNode = callNode.getRootNode();
assert callRootNode != null
: "Should be called only from Enso code, and thus, should always have a root node";
if (callRootNode instanceof EnsoRootNode ensoRootNode) {
var pkg = ensoRootNode.getModuleScope().getModule().getPackage();
// Don't return null, as that would signal to Truffle that we want to
// continue the iteration.
if (pkg != null) {
return Optional.of(pkg);
} else {
return Optional.empty();
}
} else {
throw new IllegalStateException(
"Should not reach here: callRootNode = "
+ callRootNode
+ ". Probably not called from Enso?");
}
},
// The first frame is always Enso_Project.enso_project
1);
if (pkgOpt.isPresent()) {
cachedProjectDescrRef = new WeakReference<>(
createProjectDescriptionAtom(ctx, pkgOpt.get()));
} else {
cachedProjectDescrRef = new WeakReference<>(notInModuleError(ctx));
}
cachedProjectDescr = cachedProjectDescrRef.get();
}
assert cachedProjectDescr != null;
return cachedProjectDescr;
}

@Specialization(guards = "!isNothing(module)")
@TruffleBoundary
public Object getOtherProjectDescr(
Object module,
@CachedLibrary(limit = "5") TypesLibrary typesLib) {
var ctx = EnsoContext.get(this);
if (!typesLib.hasType(module)) {
return unsupportedArgsError(module);
}
Type moduleType = typesLib.getType(module);
// Currently, the module is represented as Type with no constructors.
if (!moduleType.getConstructors().isEmpty()) {
return unsupportedArgsError(module);
}
var pkg = moduleType.getDefinitionScope().getModule().getPackage();
if (pkg != null) {
return createProjectDescriptionAtom(ctx, pkg);
} else {
return notInModuleError(ctx);
}
}

private static Atom createProjectDescriptionAtom(EnsoContext ctx, Package<TruffleFile> pkg) {
EnsoFile rootPath = new EnsoFile(pkg.root().normalize());
Object cfg = ctx.getEnvironment().asGuestValue(pkg.config());
return ctx.getBuiltins()
.getProjectDescription()
.getUniqueConstructor()
.newInstance(rootPath, cfg);
}

private DataflowError unsupportedArgsError(Object moduleActual) {
return DataflowError.withoutTrace(
EnsoContext.get(this)
.getBuiltins()
.error()
.makeUnsupportedArgumentsError(
new Object[]{moduleActual}, "The `module` argument does not refer to a module"),
this);
}

private DataflowError notInModuleError(EnsoContext ctx) {
return DataflowError.withoutTrace(
ctx.getBuiltins().error().makeModuleNotInPackageError(), this);
}

boolean isNothing(Object object) {
return EnsoContext.get(this).getBuiltins().nothing() == object;
}
}
Loading

0 comments on commit 3a42d0c

Please sign in to comment.