Skip to content

Commit

Permalink
Merge pull request #2404 from lf-lang/extending
Browse files Browse the repository at this point in the history
Angular bracket imports for reusable reactor modules
  • Loading branch information
lhstrh authored Oct 9, 2024
2 parents 7cbc49b + 6427abb commit 4e27b9a
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,6 @@ gradle-app.setting
*.jar
core/model/

# Exclude all build directories except test/Python/build for testing purposes
!test/Python/build/

13 changes: 12 additions & 1 deletion core/src/main/java/org/lflang/LFResourceDescriptionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.eclipse.xtext.scoping.impl.ImportUriResolver;
import org.eclipse.xtext.util.IAcceptor;
import org.lflang.lf.Model;
import org.lflang.util.ImportUtil;

/**
* Resource description strategy designed to limit global scope to only those files that were
Expand Down Expand Up @@ -77,7 +78,17 @@ public boolean createEObjectDescriptions(
*/
private void createEObjectDescriptionForModel(
Model model, IAcceptor<IEObjectDescription> acceptor) {
var uris = model.getImports().stream().map(uriResolver).collect(Collectors.joining(DELIMITER));
var uris =
model.getImports().stream()
.map(
importObj -> {
return (importObj.getImportURI() != null)
? importObj.getImportURI()
: ImportUtil.buildPackageURI(
importObj.getImportPackage(),
model.eResource()); // Use the resolved import string
})
.collect(Collectors.joining(DELIMITER));
var userData = Map.of(INCLUDES, uris);
QualifiedName qname = QualifiedName.create(model.eResource().getURI().toString());
acceptor.accept(EObjectDescription.create(qname, model, userData));
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/org/lflang/LinguaFranca.xtext
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Model:
/**
* Import declaration.
*/
Import: 'import' reactorClasses+=ImportedReactor (',' reactorClasses+=ImportedReactor)* 'from' importURI=STRING ';'?;
Import: 'import' reactorClasses+=ImportedReactor (',' reactorClasses+=ImportedReactor)* 'from' (importURI=STRING | '<' importPackage=Path '>') ';'?;

ReactorDecl: Reactor | ImportedReactor;

Expand Down Expand Up @@ -486,7 +486,7 @@ Code:
;

FSName:
(ID | '.' | '_')+
(ID | '.' | '_' | '-')+
;
// Absolute or relative directory path in Windows, Linux, or MacOS.
Path:
Expand Down
1 change: 1 addition & 0 deletions core/src/main/java/org/lflang/ast/IsEqual.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ public Boolean caseModel(Model object) {
public Boolean caseImport(Import object) {
return new ComparisonMachine<>(object, Import.class)
.equalAsObjects(Import::getImportURI)
.equalAsObjects(Import::getImportPackage)
.listsEquivalent(Import::getReactorClasses)
.conclusion;
}
Expand Down
8 changes: 5 additions & 3 deletions core/src/main/java/org/lflang/ast/ToLf.java
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,11 @@ public MalleableString caseImport(Import object) {
.append("import ")
// TODO: This is a place where we can use conditional parentheses.
.append(list(", ", "", "", false, true, true, object.getReactorClasses()))
.append(" from \"")
.append(object.getImportURI())
.append("\"")
.append(" from ")
.append(
object.getImportURI() != null
? "\"" + object.getImportURI() + "\""
: "<" + object.getImportPackage() + ">")
.get();
}

Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/org/lflang/ast/ToSExpr.java
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ public SExpr caseImport(Import object) {
// reactorClasses+=ImportedReactor)* 'from' importURI=STRING ';'?;
return sList(
"import",
new SAtom<>(object.getImportURI()),
new SAtom<>(
object.getImportURI() != null ? object.getImportURI() : object.getImportPackage()),
sList("reactors", object.getReactorClasses()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.lflang.federated.generator;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -9,6 +10,7 @@
import org.lflang.generator.CodeBuilder;
import org.lflang.lf.Import;
import org.lflang.lf.Model;
import org.lflang.util.ImportUtil;

/**
* Helper class to generate import statements for a federate.
Expand All @@ -31,7 +33,15 @@ String generateImports(FederateInstance federate, FederationFileConfig fileConfi
.forEach(
i -> {
visitedImports.add(i);
Path importPath = fileConfig.srcPath.resolve(i.getImportURI()).toAbsolutePath();
Path importPath =
fileConfig
.srcPath
.resolve(
i.getImportURI() != null
? Paths.get(i.getImportURI())
: ImportUtil.buildPackageURIfromSrc(
i.getImportPackage(), fileConfig.srcPath.toString()))
.toAbsolutePath();
i.setImportURI(
fileConfig.getSrcPath().relativize(importPath).toString().replace('\\', '/'));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
/**
* Global scope provider that limits access to only those files that were explicitly imported.
*
* <p>Adapted from from Xtext manual, Chapter 8.7.
* <p>Adapted from Xtext manual, Chapter 8.7.
*
* @author Marten Lohstroh
* @see <a href="https://www.eclipse.org/Xtext/documentation/2.6.0/Xtext%20Documentation.pdf">xtext
Expand Down
15 changes: 13 additions & 2 deletions core/src/main/java/org/lflang/scoping/LFScopeProviderImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
package org.lflang.scoping;

import static java.util.Collections.emptyList;
import static org.lflang.ast.ASTUtils.*;
import static org.lflang.ast.ASTUtils.allActions;
import static org.lflang.ast.ASTUtils.allInputs;
import static org.lflang.ast.ASTUtils.allOutputs;
import static org.lflang.ast.ASTUtils.allParameters;
import static org.lflang.ast.ASTUtils.allTimers;
import static org.lflang.ast.ASTUtils.allWatchdogs;
import static org.lflang.ast.ASTUtils.toDefinition;

import com.google.inject.Inject;
import java.util.ArrayList;
Expand All @@ -50,6 +56,7 @@
import org.lflang.lf.ReactorDecl;
import org.lflang.lf.VarRef;
import org.lflang.lf.Watchdog;
import org.lflang.util.ImportUtil;

/**
* This class enforces custom rules. In particular, it resolves references to parameters, ports,
Expand Down Expand Up @@ -104,7 +111,11 @@ public IScope getScope(EObject context, EReference reference) {
* statement.
*/
protected IScope getScopeForImportedReactor(ImportedReactor context, EReference reference) {
String importURI = ((Import) context.eContainer()).getImportURI();
String importURI =
((Import) context.eContainer()).getImportURI() != null
? ((Import) context.eContainer()).getImportURI()
: ImportUtil.buildPackageURI(
((Import) context.eContainer()).getImportPackage(), context.eResource());
var importedURI =
scopeProvider.resolve(importURI == null ? "" : importURI, context.eResource());
if (importedURI != null) {
Expand Down
107 changes: 107 additions & 0 deletions core/src/main/java/org/lflang/util/ImportUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package org.lflang.util;

import java.nio.file.Path;
import java.nio.file.Paths;
import org.eclipse.emf.ecore.resource.Resource;

/**
* Utility class for handling package-related URIs in the context of LF (Lingua Franca) libraries.
* This class provides methods to build URIs for accessing library files based on their location in
* a project structure, specifically targeting the "build/lfc_include" directory for library
* inclusion.
*/
public class ImportUtil {

/**
* Builds a package URI based on the provided URI string and resource. It traverses upwards from
* the current resource URI until it finds the "src" directory, then constructs the final URI
* pointing to the library file within the "build/lfc_include" directory.
*
* @param uriStr A string representing the URI of the file. It must contain both the library name
* and file name, separated by a '/'.
* @param resource The resource from which the URI resolution should start.
* @return The constructed package URI as a string.
* @throws IllegalArgumentException if the URI string does not contain both library and file
* names.
*/
public static String buildPackageURI(String uriStr, Resource resource) {
Path rootPath = Paths.get(resource.getURI().toString()).toAbsolutePath();

Path uriPath = Paths.get(uriStr.trim());

if (uriPath.getNameCount() < 2) {
throw new IllegalArgumentException("URI must contain both library name and file name.");
}

// Initialize the path as the current directory
Path finalPath = Paths.get("");

// Traverse upwards until we reach the "src/" directory
while (!rootPath.endsWith("src")) {
rootPath = rootPath.getParent();
if (rootPath == null) {
throw new IllegalArgumentException("The 'src' directory was not found in the given path.");
}
finalPath = finalPath.resolve("..");
}

// Build the final path
finalPath =
finalPath
.resolve("build")
.resolve("lfc_include")
.resolve(uriPath.getName(0))
.resolve("src")
.resolve("lib")
.resolve(uriPath.getName(1));

return finalPath.toString();
}

/**
* Builds a package URI based on the provided URI string and source path. This method works
* similarly to the `buildPackageURI`, but it accepts a direct source path instead of a resource.
* It traverses upwards to locate the "src/" directory and then constructs the URI pointing to the
* library file.
*
* @param uriStr A string representing the URI of the file. It must contain both the library name
* and file name, separated by a '/'.
* @param root The root path from which the URI resolution should start.
* @return The constructed package URI as a string.
* @throws IllegalArgumentException if the URI string or source path is null, empty, or does not
* contain both the library name and file name.
*/
public static Path buildPackageURIfromSrc(String uriStr, String root) {
if (uriStr == null || root == null || uriStr.trim().isEmpty() || root.trim().isEmpty()) {
throw new IllegalArgumentException("URI string and source path must not be null or empty.");
}

Path uriPath = Paths.get(uriStr.trim());

if (uriPath.getNameCount() < 2) {
throw new IllegalArgumentException("URI must contain both library name and file name.");
}

// Use the src path to create a base path
Path rootPath = Paths.get(root).toAbsolutePath();

// Traverse upwards until we reach the "src/" directory
while (!rootPath.endsWith("src")) {
rootPath = rootPath.getParent();
if (rootPath == null) {
throw new IllegalArgumentException("The 'src' directory was not found in the given path.");
}
}

Path finalPath =
rootPath
.resolveSibling("build")
.resolve("lfc_include")
.resolve(uriPath.getName(0)) // library name
.resolve("src")
.resolve("lib")
.resolve(uriPath.getName(1)); // file name

return finalPath;
}
}
12 changes: 12 additions & 0 deletions test/Python/build/lfc_include/library-test/src/lib/Import.lf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
target Python

reactor Count(offset=0, period = 1 sec) {
state count = 1
output out
timer t(offset, period)

reaction(t) -> out {=
out.set(self.count)
self.count += 1
=}
}
21 changes: 21 additions & 0 deletions test/Python/src/LingoFederatedImport.lf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Test the new import statement for Lingo downloaded packages with the import path enclosed in angle brackets
# Version 1: The LF file is located in "src".
target Python {
timeout: 2 sec
}

import Count from <library-test/Import.lf>

reactor Actuator {
input results

reaction(results) {=
print(f"Count: {results.value}")
=}
}

federated reactor {
count = new Count()
act = new Actuator()
count.out -> act.results
}
21 changes: 21 additions & 0 deletions test/Python/src/lingo_imports/FederatedTestImportPackages.lf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Test the new import statement for Lingo downloaded packages with the import path enclosed in angle brackets
# Version 2: The LF file is now located in a subdirectory under "src".
target Python {
timeout: 2 sec
}

import Count from <library-test/Import.lf>

reactor Actuator {
input results

reaction(results) {=
print(f"Count: {results.value}")
=}
}

federated reactor {
count = new Count()
act = new Actuator()
count.out -> act.results
}
20 changes: 20 additions & 0 deletions test/Python/src/lingo_imports/TestImportPackages.lf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Test the new import statement for Lingo downloaded packages with the import path enclosed in angle brackets
target Python {
timeout: 2 sec
}

import Count from <library-test/Import.lf>

reactor Actuator {
input results

reaction(results) {=
print(f"Count: {results.value}")
=}
}

main reactor {
count = new Count()
act = new Actuator()
count.out -> act.results
}

0 comments on commit 4e27b9a

Please sign in to comment.