diff --git a/.gitignore b/.gitignore index da8902f971..193c7b8a77 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,6 @@ gradle-app.setting *.jar core/model/ +# Exclude all build directories except test/Python/build for testing purposes +!test/Python/build/ + diff --git a/core/src/main/java/org/lflang/LFResourceDescriptionStrategy.java b/core/src/main/java/org/lflang/LFResourceDescriptionStrategy.java index 487e63374b..db055a7df8 100644 --- a/core/src/main/java/org/lflang/LFResourceDescriptionStrategy.java +++ b/core/src/main/java/org/lflang/LFResourceDescriptionStrategy.java @@ -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 @@ -77,7 +78,17 @@ public boolean createEObjectDescriptions( */ private void createEObjectDescriptionForModel( Model model, IAcceptor 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)); diff --git a/core/src/main/java/org/lflang/LinguaFranca.xtext b/core/src/main/java/org/lflang/LinguaFranca.xtext index ba45b3bd0c..a52f9fec23 100644 --- a/core/src/main/java/org/lflang/LinguaFranca.xtext +++ b/core/src/main/java/org/lflang/LinguaFranca.xtext @@ -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; @@ -478,7 +478,7 @@ Code: ; FSName: - (ID | '.' | '_')+ + (ID | '.' | '_' | '-')+ ; // Absolute or relative directory path in Windows, Linux, or MacOS. Path: diff --git a/core/src/main/java/org/lflang/ast/IsEqual.java b/core/src/main/java/org/lflang/ast/IsEqual.java index 8eec27a430..afe1568f96 100644 --- a/core/src/main/java/org/lflang/ast/IsEqual.java +++ b/core/src/main/java/org/lflang/ast/IsEqual.java @@ -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; } diff --git a/core/src/main/java/org/lflang/ast/ToLf.java b/core/src/main/java/org/lflang/ast/ToLf.java index 3b1b1e48df..5f32920d9b 100644 --- a/core/src/main/java/org/lflang/ast/ToLf.java +++ b/core/src/main/java/org/lflang/ast/ToLf.java @@ -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(); } diff --git a/core/src/main/java/org/lflang/ast/ToSExpr.java b/core/src/main/java/org/lflang/ast/ToSExpr.java index a514d98234..a2047f5280 100644 --- a/core/src/main/java/org/lflang/ast/ToSExpr.java +++ b/core/src/main/java/org/lflang/ast/ToSExpr.java @@ -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())); } diff --git a/core/src/main/java/org/lflang/federated/generator/FedImportEmitter.java b/core/src/main/java/org/lflang/federated/generator/FedImportEmitter.java index 59b2fae58b..883bc8d327 100644 --- a/core/src/main/java/org/lflang/federated/generator/FedImportEmitter.java +++ b/core/src/main/java/org/lflang/federated/generator/FedImportEmitter.java @@ -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; @@ -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. @@ -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('\\', '/')); }); diff --git a/core/src/main/java/org/lflang/scoping/LFGlobalScopeProvider.java b/core/src/main/java/org/lflang/scoping/LFGlobalScopeProvider.java index 18c7a3a8af..6c58f67665 100644 --- a/core/src/main/java/org/lflang/scoping/LFGlobalScopeProvider.java +++ b/core/src/main/java/org/lflang/scoping/LFGlobalScopeProvider.java @@ -39,7 +39,7 @@ /** * Global scope provider that limits access to only those files that were explicitly imported. * - *

Adapted from from Xtext manual, Chapter 8.7. + *

Adapted from Xtext manual, Chapter 8.7. * * @author Marten Lohstroh * @see xtext diff --git a/core/src/main/java/org/lflang/scoping/LFScopeProviderImpl.java b/core/src/main/java/org/lflang/scoping/LFScopeProviderImpl.java index 3455c1d822..e45c25137c 100644 --- a/core/src/main/java/org/lflang/scoping/LFScopeProviderImpl.java +++ b/core/src/main/java/org/lflang/scoping/LFScopeProviderImpl.java @@ -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; @@ -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, @@ -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) { diff --git a/core/src/main/java/org/lflang/util/ImportUtil.java b/core/src/main/java/org/lflang/util/ImportUtil.java new file mode 100644 index 0000000000..b7de0a16d4 --- /dev/null +++ b/core/src/main/java/org/lflang/util/ImportUtil.java @@ -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; + } +} diff --git a/test/Python/build/lfc_include/library-test/src/lib/Import.lf b/test/Python/build/lfc_include/library-test/src/lib/Import.lf new file mode 100644 index 0000000000..97ca1fb73f --- /dev/null +++ b/test/Python/build/lfc_include/library-test/src/lib/Import.lf @@ -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 + =} +} \ No newline at end of file diff --git a/test/Python/src/LingoFederatedImport.lf b/test/Python/src/LingoFederatedImport.lf new file mode 100644 index 0000000000..5b3a4fadd6 --- /dev/null +++ b/test/Python/src/LingoFederatedImport.lf @@ -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 + +reactor Actuator { + input results + + reaction(results) {= + print(f"Count: {results.value}") + =} +} + +federated reactor { + count = new Count() + act = new Actuator() + count.out -> act.results +} diff --git a/test/Python/src/lingo_imports/FederatedTestImportPackages.lf b/test/Python/src/lingo_imports/FederatedTestImportPackages.lf new file mode 100644 index 0000000000..4081aa29ba --- /dev/null +++ b/test/Python/src/lingo_imports/FederatedTestImportPackages.lf @@ -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 + +reactor Actuator { + input results + + reaction(results) {= + print(f"Count: {results.value}") + =} +} + +federated reactor { + count = new Count() + act = new Actuator() + count.out -> act.results +} diff --git a/test/Python/src/lingo_imports/TestImportPackages.lf b/test/Python/src/lingo_imports/TestImportPackages.lf new file mode 100644 index 0000000000..fc0915f160 --- /dev/null +++ b/test/Python/src/lingo_imports/TestImportPackages.lf @@ -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 + +reactor Actuator { + input results + + reaction(results) {= + print(f"Count: {results.value}") + =} +} + +main reactor { + count = new Count() + act = new Actuator() + count.out -> act.results +}