package org.simantics.scl.compiler.module.repository; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.simantics.scl.compiler.common.exceptions.InternalCompilerError; import org.simantics.scl.compiler.compilation.CompilationContext; import org.simantics.scl.compiler.elaboration.modules.SCLValue; import org.simantics.scl.compiler.environment.ConcreteEnvironment; import org.simantics.scl.compiler.environment.EmptyEnvironment; import org.simantics.scl.compiler.environment.Environment; import org.simantics.scl.compiler.environment.NamespaceImpl.ModuleImport; import org.simantics.scl.compiler.environment.NamespaceSpec; import org.simantics.scl.compiler.environment.filter.NamespaceFilter; import org.simantics.scl.compiler.environment.filter.NamespaceFilters; import org.simantics.scl.compiler.environment.specification.EnvironmentSpecification; import org.simantics.scl.compiler.errors.CompilationError; import org.simantics.scl.compiler.errors.DoesNotExist; import org.simantics.scl.compiler.errors.Failable; import org.simantics.scl.compiler.errors.Failure; import org.simantics.scl.compiler.errors.Locations; import org.simantics.scl.compiler.errors.Success; import org.simantics.scl.compiler.module.ImportDeclaration; import org.simantics.scl.compiler.module.Module; import org.simantics.scl.compiler.module.options.ModuleCompilationOptionsAdvisor; import org.simantics.scl.compiler.module.repository.UpdateListener.Observable; import org.simantics.scl.compiler.runtime.RuntimeEnvironment; import org.simantics.scl.compiler.runtime.RuntimeEnvironmentImpl; import org.simantics.scl.compiler.runtime.RuntimeModule; import org.simantics.scl.compiler.runtime.RuntimeModuleMap; import org.simantics.scl.compiler.source.ModuleSource; import org.simantics.scl.compiler.source.repository.ModuleSourceRepository; import org.simantics.scl.compiler.top.ModuleInitializer; import org.simantics.scl.compiler.top.SCLCompilerConfiguration; import org.simantics.scl.compiler.top.ValueNotFound; import org.simantics.scl.compiler.types.Types; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import gnu.trove.map.hash.THashMap; import gnu.trove.map.hash.TObjectLongHashMap; import gnu.trove.procedure.TObjectObjectProcedure; import gnu.trove.set.hash.THashSet; /** * Manages compilation and caching of SCL modules. * * @author Hannu Niemistö */ public class ModuleRepository { private static final Logger LOGGER = LoggerFactory.getLogger(ModuleRepository.class); private final ModuleRepository parentRepository; private final ModuleSourceRepository sourceRepository; private ConcurrentHashMap moduleCache = new ConcurrentHashMap(); private static final ThreadLocal> PENDING_MODULES = new ThreadLocal>(); private ModuleCompilationOptionsAdvisor advisor = null; private static void beginModuleCompilation(String moduleName) { THashSet set = PENDING_MODULES.get(); if(set == null) { set = new THashSet(); PENDING_MODULES.set(set); } if(!set.add(moduleName)) throw new IllegalArgumentException("Cyclic module dependency detected at " + moduleName + "."); } private static void finishModuleCompilation(String moduleName) { PENDING_MODULES.get().remove(moduleName); } private class ModuleEntry extends UpdateListener implements Observable { final String moduleName; THashSet listeners = new THashSet(); // listeners == null is used as a marker that this entry is disposed // should be handled only inside synchronized code ModuleSource source; Failable compilationResult; Failable runtimeModule; // created lazily public ModuleEntry(String moduleName) { this.moduleName = moduleName; } synchronized void addListener(UpdateListener listener) { if(listener == null || listeners == null) return; listeners.add(listener); listener.addObservable(this); } public synchronized void removeListener(UpdateListener listener) { if (listeners == null) return; listeners.remove(listener); } @Override public void notifyAboutUpdate() { // There is a chance that another observable calls notifyAboutUpdate() before stopListening has been completed, // but notifyAboutUpdate(ArrayList) lets only one thread to do the notification of dependencies // by clearing listeners field. stopListening(); ArrayList externalListeners = new ArrayList(); notifyAboutUpdate(externalListeners); for(UpdateListener listener : externalListeners) listener.notifyAboutUpdate(); } void notifyAboutUpdate(ArrayList externalListeners) { THashSet listenersCopy; synchronized(this) { listenersCopy = listeners; if (listenersCopy == null) return; listeners = null; } if(moduleCache.get(moduleName) == this) { moduleCache.remove(moduleName); if(SCLCompilerConfiguration.TRACE_MODULE_UPDATE) { LOGGER.info("Invalidate " + moduleName); for(UpdateListener l : listenersCopy) LOGGER.info(" " + l); } for(UpdateListener l : listenersCopy) if(!l.stopListening()) ; else if(l instanceof ModuleEntry) ((ModuleEntry)l).notifyAboutUpdate(externalListeners); else externalListeners.add(l); } } private ModuleEntry initModuleEntryAndAddListener(UpdateListener listener) { source = sourceRepository.getModuleSource(moduleName, this); if(source == null) compilationResult = DoesNotExist.getInstance(); else { if(SCLCompilerConfiguration.TRACE_MODULE_UPDATE) LOGGER.info("Compile " + source); beginModuleCompilation(moduleName); compilationResult = source.compileModule(ModuleRepository.this, this, advisor == null ? null : advisor.getOptions(moduleName)); finishModuleCompilation(moduleName); } ModuleEntry oldEntry = moduleCache.putIfAbsent(moduleName, this); if(oldEntry != null) { oldEntry.addListener(listener); return oldEntry; } addListener(listener); return this; } @SuppressWarnings({ "rawtypes", "unchecked" }) public synchronized Failable getRuntimeModule() { if(runtimeModule == null) { if(compilationResult.didSucceed()) { Module module = compilationResult.getResult(); RuntimeModuleMap parentModules = new RuntimeModuleMap(); if(!moduleName.equals(Types.BUILTIN)) { parentModules.add(ModuleRepository.this.getRuntimeModule(Types.BUILTIN) .getResult()); Collection dependencies = module.getDependencies(); THashMap moduleEntries; try { moduleEntries = getModuleEntries(null, dependencies.toArray(new ImportDeclaration[dependencies.size()]), null, false); } catch (ImportFailureException e) { throw new InternalCompilerError(e); } for(RuntimeModule m : mapEntriesToRuntimeModules(moduleEntries).values()) parentModules.add(m); } /*for(ImportDeclaration importAst : module.getDependencies()) { RuntimeModule parentModule = ModuleRepository.this.getRuntimeModule(importAst.moduleName) .getResult(); if(parentModule != null) parentModules.add(parentModule); }*/ RuntimeModule rm = new RuntimeModule(module, parentModules, module.getParentClassLoader()); ModuleInitializer initializer = module.getModuleInitializer(); if(initializer != null) try { initializer.initializeModule(rm.getMutableClassLoader().getClassLoader()); } catch (Exception e) { compilationResult = new Failure(new CompilationError[] {new CompilationError("Initialization of module " + moduleName + " failed: " + e.getMessage())}); e.printStackTrace(); } runtimeModule = new Success(rm); } else runtimeModule = (Failable)(Failable)compilationResult; } return runtimeModule; } public synchronized void dispose() { listeners = null; stopListening(); source = null; compilationResult = null; if (runtimeModule != null) { if (runtimeModule.didSucceed()) runtimeModule.getResult().dispose(); } runtimeModule = null; } @Override public String toString() { return "ModuleEntry@" + moduleName + "@" + hashCode(); } } public ModuleRepository(ModuleRepository parentRepository, ModuleSourceRepository sourceRepository) { this.parentRepository = parentRepository; this.sourceRepository = sourceRepository; } public ModuleRepository(ModuleSourceRepository sourceRepository) { this(null, sourceRepository); } public Failable getModule(String moduleName, UpdateListener listener) { return getModuleEntry(moduleName, listener).compilationResult; } public Failable getModule(String moduleName) { return getModule(moduleName, null); } public void update(String moduleName) { getModuleEntry(moduleName, null).notifyAboutUpdate(); } public Failable getRuntimeModule(String moduleName, UpdateListener listener) { return getModuleEntry(moduleName, listener).getRuntimeModule(); } public Failable getRuntimeModule(String moduleName) { return getRuntimeModule(moduleName, null); } private ModuleEntry getModuleEntry(String moduleName, UpdateListener listener) { /* It is deliberate that the following code does not try to prevent * simultaneous compilation of the same module. This is because in * some situations only certain thread trying compilation can succeed * in it. */ ModuleEntry entry = moduleCache.get(moduleName); if(entry == null) entry = new ModuleEntry(moduleName).initModuleEntryAndAddListener(listener); else entry.addListener(listener); if(entry.compilationResult == DoesNotExist.INSTANCE && parentRepository != null) return parentRepository.getModuleEntry(moduleName, listener); else return entry; } private THashMap getModuleEntries( CompilationContext compilationContext, ImportDeclaration[] imports, UpdateListener listener, boolean robustly) throws ImportFailureException { THashMap result = new THashMap(); Collection failures = null; TObjectLongHashMap originalImports = new TObjectLongHashMap(); ArrayList stack = new ArrayList(imports.length); for(ImportDeclaration import_ : imports) { stack.add(import_); originalImports.put(import_.moduleName, import_.location); } while(!stack.isEmpty()) { ImportDeclaration import_ = stack.remove(stack.size()-1); if(!result.containsKey(import_.moduleName)) { boolean originalImport = originalImports.contains(import_.moduleName); ModuleEntry entry = getModuleEntry(import_.moduleName, originalImport ? listener : null); Failable compilationResult = entry.compilationResult; if(compilationResult.didSucceed()) { result.put(import_.moduleName, entry); stack.addAll(compilationResult.getResult().getDependencies()); if(originalImport) { String deprecation = compilationResult.getResult().getDeprecation(); if(deprecation != null && compilationContext != null) { long location = originalImport ? originalImports.get(import_.moduleName) : Locations.NO_LOCATION; compilationContext.errorLog.logWarning(location, "Deprecated module " + import_.moduleName + (deprecation.isEmpty() ? "." : ": " + deprecation)); } } } else { if(failures == null) failures = new ArrayList(2); failures.add(new ImportFailure(import_.location, import_.moduleName, compilationResult == DoesNotExist.INSTANCE ? ImportFailure.MODULE_DOES_NOT_EXIST_REASON : ((Failure)compilationResult).errors)); } } } if(failures != null && !robustly) throw new ImportFailureException(failures); return result; } private static THashMap mapEntriesToModules(THashMap entries) { final THashMap result = new THashMap(entries.size()); entries.forEachEntry(new TObjectObjectProcedure() { @Override public boolean execute(String a, ModuleEntry b) { result.put(a, b.compilationResult.getResult()); return true; } }); return result; } private static THashMap mapEntriesToRuntimeModules(THashMap entries) { final THashMap result = new THashMap(entries.size()); entries.forEachEntry(new TObjectObjectProcedure() { @Override public boolean execute(String a, ModuleEntry b) { result.put(a, b.getRuntimeModule().getResult()); return true; } }); return result; } public Environment createEnvironment( ImportDeclaration[] imports, UpdateListener listener) throws ImportFailureException { return createEnvironment(null, imports, listener); } public Environment createEnvironment( CompilationContext compilationContext, ImportDeclaration[] imports, UpdateListener listener) throws ImportFailureException { THashMap entries = getModuleEntries(compilationContext, imports, listener, false); THashMap moduleMap = mapEntriesToModules(entries); return createEnvironment(moduleMap, imports); } public Environment createEnvironmentRobustly( CompilationContext compilationContext, ImportDeclaration[] imports, UpdateListener listener) { try { THashMap entries = getModuleEntries(compilationContext, imports, listener, true); THashMap moduleMap = mapEntriesToModules(entries); return createEnvironment(moduleMap, imports); } catch(ImportFailureException e) { // Should not happen because of robust flag return EmptyEnvironment.INSTANCE; } } public Environment createEnvironment( EnvironmentSpecification specification, UpdateListener listener) throws ImportFailureException { return createEnvironment(specification.imports.toArray(new ImportDeclaration[specification.imports.size()]), listener); } public RuntimeEnvironment createRuntimeEnvironment( EnvironmentSpecification environmentSpecification, ClassLoader parentClassLoader) throws ImportFailureException { return createRuntimeEnvironment(environmentSpecification, parentClassLoader, null); } public RuntimeEnvironment createRuntimeEnvironment(EnvironmentSpecification environmentSpecification) throws ImportFailureException { return createRuntimeEnvironment(environmentSpecification, getClass().getClassLoader()); } public RuntimeEnvironment createRuntimeEnvironment( EnvironmentSpecification environmentSpecification, ClassLoader parentClassLoader, UpdateListener listener) throws ImportFailureException { return createRuntimeEnvironment( environmentSpecification.imports.toArray(new ImportDeclaration[environmentSpecification.imports.size()]), parentClassLoader, listener); } public RuntimeEnvironment createRuntimeEnvironment( ImportDeclaration[] imports, ClassLoader parentClassLoader, UpdateListener listener) throws ImportFailureException { THashMap entries = getModuleEntries(null, imports, listener, false); THashMap moduleMap = mapEntriesToModules(entries); Environment environment = createEnvironment(moduleMap, imports); THashMap runtimeModuleMap = mapEntriesToRuntimeModules(entries); return new RuntimeEnvironmentImpl(environment, parentClassLoader, runtimeModuleMap); } private static Environment createEnvironment( THashMap moduleMap, ImportDeclaration[] imports) { NamespaceSpec spec = new NamespaceSpec(); for(ImportDeclaration import_ : imports) if(import_.localName != null) addToNamespace(moduleMap, spec, import_.moduleName, import_.localName, NamespaceFilters.createFromSpec(import_.spec)); return new ConcreteEnvironment(moduleMap, spec.toNamespace()); } private static void addToNamespace(THashMap moduleMap, NamespaceSpec namespace, String moduleName, String localName, NamespaceFilter filter) { if(localName.isEmpty()) addToNamespace(moduleMap, namespace, moduleName, filter); else addToNamespace(moduleMap, namespace.getNamespace(localName), moduleName, filter); } private static void addToNamespace(THashMap moduleMap, NamespaceSpec namespace, String moduleName, NamespaceFilter filter) { ModuleImport moduleImport = namespace.moduleMap.get(moduleName); if(moduleImport == null) { Module module = moduleMap.get(moduleName); namespace.moduleMap.put(moduleName, new ModuleImport(module, filter)); for(ImportDeclaration import_ : module.getDependencies()) if(import_.localName != null) { NamespaceFilter localFilter = NamespaceFilters.createFromSpec(import_.spec); if(import_.localName.equals("")) localFilter = NamespaceFilters.intersection(filter, localFilter); addToNamespace(moduleMap, namespace, import_.moduleName, import_.localName, localFilter); } } else if(!filter.isSubsetOf(moduleImport.filter)) { moduleImport.filter = NamespaceFilters.union(moduleImport.filter, filter); for(ImportDeclaration import_ : moduleImport.module.getDependencies()) // We have to recheck only modules imported to this namespace if("".equals(import_.localName)) { NamespaceFilter localFilter = NamespaceFilters.createFromSpec(import_.spec); localFilter = NamespaceFilters.intersection(filter, localFilter); addToNamespace(moduleMap, namespace, import_.moduleName, import_.localName, localFilter); } } } public Object getValue(String moduleName, String valueName) throws ValueNotFound { Failable module = getRuntimeModule(moduleName); if(module.didSucceed()) return module.getResult().getValue(valueName); else if(module == DoesNotExist.INSTANCE) throw new ValueNotFound("Didn't find module " + moduleName); else throw new ValueNotFound(((Failure)module).toString()); } public Object getValue(String fullValueName) throws ValueNotFound { int p = fullValueName.lastIndexOf('/'); if(p < 0) throw new ValueNotFound(fullValueName + " is not a valid full value name."); return getValue(fullValueName.substring(0, p), fullValueName.substring(p+1)); } public SCLValue getValueRef(String moduleName, String valueName) throws ValueNotFound { Failable module = getModule(moduleName); if(module.didSucceed()) { SCLValue value = module.getResult().getValue(valueName); if(value == null) throw new ValueNotFound("Module " + moduleName + " does not contain value " + valueName + "."); return value; } else if(module == DoesNotExist.INSTANCE) throw new ValueNotFound("Didn't find module " + moduleName); else throw new ValueNotFound(((Failure)module).toString()); } public SCLValue getValueRef(String fullValueName) throws ValueNotFound { int p = fullValueName.lastIndexOf('/'); if(p < 0) throw new ValueNotFound(fullValueName + " is not a valid full value name."); return getValueRef(fullValueName.substring(0, p), fullValueName.substring(p+1)); } public ModuleSourceRepository getSourceRepository() { return sourceRepository; } public String getDocumentation(String documentationName) { String documentation = sourceRepository.getDocumentation(documentationName); if(documentation == null && parentRepository != null) return parentRepository.getDocumentation(documentationName); return documentation; } /** * Flush clears module repository cache completely. It should not be called in * normal operation, but may be useful during testing for clearing repositories * that are statically defined. */ public void flush() { if (parentRepository != null) parentRepository.flush(); if (moduleCache != null) for (ModuleEntry entry : moduleCache.values()) entry.dispose(); moduleCache = new ConcurrentHashMap(); } /** * Gets the map of all modules that have been currently compiled successfully. * Not that the method does not return all possible modules in the source repository. */ public Map getModules() { Map result = new HashMap<>(moduleCache.size()); for (Map.Entry entry : moduleCache.entrySet()) { ModuleEntry moduleEntry = entry.getValue(); if (moduleEntry.compilationResult.didSucceed()) { result.put(entry.getKey(), moduleEntry.compilationResult.getResult()); } } return result; } public ModuleCompilationOptionsAdvisor getAdvisor() { return advisor; } public void setAdvisor(ModuleCompilationOptionsAdvisor advisor) { this.advisor = advisor; } }