core/kernel/source/jetbrains/mps/smodel/Language.java (317 lines of code) (raw):

/* * Copyright 2003-2025 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.smodel; import jetbrains.mps.extapi.module.SRepositoryExt; import jetbrains.mps.logging.Logger; import jetbrains.mps.module.ReloadableModule; import jetbrains.mps.module.SDependencyImpl; import jetbrains.mps.project.AbstractModule; import jetbrains.mps.project.io.DescriptorIO; import jetbrains.mps.project.io.DescriptorIOFacade; import jetbrains.mps.project.structure.modules.GeneratorDescriptor; import jetbrains.mps.project.structure.modules.LanguageDescriptor; import jetbrains.mps.project.structure.modules.ModuleDescriptor; import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory; import jetbrains.mps.smodel.language.LanguageAspectSupport; import jetbrains.mps.util.IterableUtil; import jetbrains.mps.vfs.IFile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SLanguage; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.module.SDependency; import org.jetbrains.mps.openapi.module.SDependencyScope; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; public class Language extends AbstractModule implements ReloadableModule { /** * Default, although not mandatory location we save our models to. * Made public just for the sake of tests. */ public static final String LANGUAGE_MODELS = "models"; /** * @deprecated Use of default value to detect aspect source root or to check module existence is wrong. */ @Deprecated(since = "3.3", forRemoval = true) public static final String LEGACY_LANGUAGE_MODELS = "languageModels"; @NotNull private LanguageDescriptor myLanguageDescriptor; // modifications are guarded with model write lock, assertCanChange() private final List<Generator> myAttachedGenerators = new ArrayList<>(2); protected Language(@NotNull LanguageDescriptor descriptor, @Nullable IFile file) { super(file); myLanguageDescriptor = descriptor; setModuleReference(descriptor.getModuleReference()); } @Override public void reloadAfterDescriptorChange() { super.reloadAfterDescriptorChange(); revalidateGenerators(); } public void addExtendedLanguage(@NotNull SModuleReference langRef) { if (this.getModuleReference().equals(langRef) || myLanguageDescriptor.getExtendedLanguages().contains(langRef)) { return; } LanguageDescriptor moduleDescriptor = getModuleDescriptor(); moduleDescriptor.getExtendedLanguages().add(langRef); setChanged(); fireChanged(); } public Set<SModuleReference> getExtendedLanguageRefs() { HashSet<SModuleReference> res = new HashSet<>(myLanguageDescriptor.getExtendedLanguages()); if (!BootstrapLanguages.coreLanguageRef().equals(getModuleReference())) { //this is needed now as we don't force the user to have an explicit dependency on core res.add(BootstrapLanguages.coreLanguageRef()); } return res; } @Override public Iterable<SDependency> getDeclaredDependencies() { HashSet<SDependency> rv = new HashSet<>(IterableUtil.asCollection(super.getDeclaredDependencies())); final SRepository repo = getRepository(); for (SModuleReference language : getExtendedLanguageRefs()) { // XXX not clear whether it's worth including implicit "extends lang.core" (see getExtendedLanguageRefs()) // or adhere to 'declared' in the name of getDeclaredDependencies and use myLanguageDescriptor.getExtendedLanguages() only rv.add(new SDependencyImpl(language, repo, SDependencyScope.EXTENDS, true)); } return rv; } /** * All the language modules extended by this one within the same repository this module is attached to. * For detached module, the set returned is empty. To access 'raw' information about extended languages, * one could use {@link #getExtendedLanguageRefs()}. * * This method requires model read access as it resolves modules. * * IMPORTANT: if any extended language is missing from the repository of the module, it's simply ignored and not included into outcome * (nor the closure of its extended languages). * * NOTE, implementation hides cyclic dependencies between languages, e.g if "A extends B extends A", * you'd get "A extends B" for A and "B extends A" for B. */ @NotNull public Set<Language> getAllExtendedLanguages() { HashSet<Language> languages = new HashSet<>(); final SRepository repository = getRepository(); if (repository == null) { return languages; } ArrayDeque<Language> queue = new ArrayDeque<>(); queue.add(this); do { Language current = queue.poll(); if (!languages.add(current)) { continue; } for (SModuleReference lr : current.getExtendedLanguageRefs()) { final SModule l = lr.resolve(repository); if (l instanceof Language) { queue.add((Language) l); } } } while (!queue.isEmpty()); return languages; } public Collection<SModuleReference> getRuntimeModulesReferences() { return Collections.unmodifiableSet(myLanguageDescriptor.getRuntimeModules()); } @Override public void attach(@NotNull SRepository repository) { super.attach(repository); final SLanguage mineDeployedIdentity = MetaAdapterFactory.getLanguage(getModuleReference()); for (Generator generator : new ModuleRepositoryFacade(repository).getAllModules(Generator.class)) { if (!mineDeployedIdentity.equals(generator.sourceLanguage())) { continue; } generator.setSourceLanguageInstance(this); } } /* * Update repository generator modules associated with this language with descriptors known to the language (registers new generators, if necessary) * This is another place in addition to ModulesMiner that knows about language-generator MD containment */ private void revalidateGenerators() { if (myLanguageDescriptor.getDeploymentDescriptor() != null) { // do not process generators listed in a source descriptor of a deployed language, assume generators // are managed on their own (use of source MD in case of deployed module is sort of design defect we can hardly fix, but at least // we shall not use information sored therein if we know that generators are treated separately when language got module.xml DD) // Perhaps, this could be approached in another way, by using getOwnedGenerators (see below) and not reporting GD for a LD read from // source along with language's DD (ModulesMiner#loadDeploymentDescriptor). I like this approach more as it would keep knowledge // about deployment inside MM, however, I'm quite sure that would ruin some code that relies on Language's knowledge about its generators. return; } if (getRepository() == null) { // detached module, can not do anything about registration/un-registration // FIXME perhaps, whole reloadAfterDescriptorChange has to be guarded and get executed for attached modules only, including facet reload // However, it's too much of a change right before the release, shall try in master, instead. // Besides, CopyModuleHelper approach with model roots copied for instantiated modules is dubious, why can't // we copy model root descriptors instead, and have it done prior to module instantiation? In this case, // we build whole descriptor first, instantiate and register with project, no chances to get here w/o detached module instance. return; } // Fair implementation shall deal with getOwnedGenerators() only, however, at the moment, Generator module needs its source Language module // and it's tricky to write external code that would deal with standalone/external generators when language's MD changes (there's no proper notification // mechanism or anything else to react to MD change). That's why we control all generators associated with the language here. // // Another important note here is that getOwnedGenerators() in its present state may not give proper answer (e.g. if a changed MD doesn't list a generator // module that has been previously exposed LinkedList<Generator> existingGenerators = new LinkedList<>(getGenerators()); SRepositoryExt moduleRepository = (SRepositoryExt) getRepository(); ModuleRepositoryFacade mrf = new ModuleRepositoryFacade(moduleRepository); for (GeneratorDescriptor nextDescriptor : myLanguageDescriptor.getGenerators()) { Generator nextGenerator = null; for (Iterator<Generator> it = existingGenerators.iterator(); it.hasNext(); ) { // looking for the existing generator with same ID Generator nextGeneratorCandidate = it.next(); GeneratorDescriptor nextGeneratorCandidateDescriptor = nextGeneratorCandidate.getModuleDescriptor(); if (Objects.equals(nextGeneratorCandidateDescriptor.getId(), nextDescriptor.getId())) { nextGenerator = nextGeneratorCandidate; it.remove(); break; } } if (nextGenerator != null) { nextGenerator.updateGeneratorDescriptor(nextDescriptor); } else { // new generator is registered with the same owners as this language // at the moment, we may use old cons that doesn't take explicit descriptor file (as it uses the one from this language, which is what we need there), // but it's fun to try a new one Generator generator = new Generator(MetaAdapterFactory.getLanguage(getModuleReference()), nextDescriptor, getDescriptorFile(), this); for (MPSModuleOwner moduleOwner : mrf.getModuleOwners(this)) { moduleRepository.registerModule(generator, moduleOwner); } } } // stale generator modules are unregistered from all owners // here we assume standalone generator modules could exist without their language // therefore we unregister only generator modules that are not standalone, assuming they originally came from MD of this very language. for (Generator stale : existingGenerators) { if (stale.getModuleDescriptor().isStandaloneModule()) { continue; } mrf.unregisterModule(stale); } } @Override public void dispose() { // though MPSModuleRepository.doUnregisterModule() cares to unregister language's generators properly, seems it // doesn't hurt to try to unregister them here as well. Either it would end up as no-op for an empty collection, or would keep // repository consistent (in case dispose() has been reached not through MPSModuleRepository) // final SRepository repo = getRepository(); // if (repo != null) { // final ModuleRepositoryFacade mrf = new ModuleRepositoryFacade(repo); // // see revalidateGenerators(), above, for reasons why we unregister all associated generators, not only directly owned. // getGenerators().forEach(mrf::unregisterModule); // } super.dispose(); } @NotNull @Override public LanguageDescriptor getModuleDescriptor() { return myLanguageDescriptor; } @Override public void doSetModuleDescriptor(ModuleDescriptor moduleDescriptor) { assert moduleDescriptor instanceof LanguageDescriptor; myLanguageDescriptor = (LanguageDescriptor) moduleDescriptor; SModuleReference reference = new jetbrains.mps.project.structure.modules.ModuleReference(myLanguageDescriptor.getNamespace(), myLanguageDescriptor.getId()); setModuleReference(reference); if (getRepository() instanceof MPSModuleRepository) { ((MPSModuleRepository) getRepository()).invalidateCaches(); } } public int getLanguageVersion() { return getModuleDescriptor().getLanguageVersion(); } public void setLanguageVersion(int version) { getModuleDescriptor().setLanguageVersion(version); fireChanged(); setChanged(); } /** * @return all generators that treat this language as their source one. */ public Collection<Generator> getGenerators() { assertCanRead(); // Language module now tracks Generator modules it is owner to. Generator modules, once attached to a repo, tell their source language they are here // with #register(Generator), and tell they are gone with #unregister(Generator). return new ArrayList<>(myAttachedGenerators); } /** * PROVISIONAL API, DON'T USE UNLESS YOU'RE 100% SURE WHAT IS THE REASON FOR THAT, AND WHAT'S THE (UPCOMING) DIFFERENCE WITH {@link #getGenerators()} * NOTE: BE CAREFUL WHEN INVOKED FROM A CODE THAT REACTS TO MODULE DESCRIPTOR CHANGES * if invoked with a changed MD, gives state according to MD contents, and not the one exposed in the repository (think about scenario when * a repo-registered, language-owned generator has been removed from MD. This method would give empty set despite the fact generator module is there) * @return generators declared and controlled by this language module. */ public Collection<Generator> getOwnedGenerators() { Set<SModuleReference> ownedGenerators = getModuleDescriptor().getGenerators().stream().map(ModuleDescriptor::getModuleReference).collect(Collectors.toSet()); return getGenerators().stream().filter(g -> ownedGenerators.contains(g.getModuleReference())).collect(Collectors.toList()); } /** * @deprecated method is not bad per se (Language module could tell SNode with concept declaration. However, * it silently excludes Interface concepts, and likely its uses need attention and switch to SConcept. * Then, we could decide whether we truly need access to language's concept nodes this way, or shall use * LanguageAspects instead. */ @Deprecated(since = "3.4", forRemoval = true) public List<SNode> getConceptDeclarations() { // FIXME there are uses in mbeddr SModel structureModel = getStructureModelDescriptor(); if (structureModel == null) return Collections.emptyList(); return FastNodeFinderManager.get(structureModel).getNodes(SNodeUtil.concept_ConceptDeclaration, true); } public List<SModel> getUtilModels() { Set<SModel> models = new HashSet<>(getModels()); models.removeAll(LanguageAspectSupport.getAspectModels(this)); models.removeAll(getAccessoryModels()); List<SModel> result = new ArrayList<>(models.size()); for (SModel md : models) { if (SModelStereotype.isStubModel(md) || SModelStereotype.isDescriptorModel(md)) { // perhaps, we need more generic isPredefinedStereotypeMPS() continue; } result.add((md)); } return result; } public SModel getStructureModelDescriptor() { return LanguageAspect.STRUCTURE.get(this); } /** * fixme why generator saves language?? * generator is contained in language it must be the other way around! */ @Override public void save() { super.save(); if (isReadOnly() || getDescriptorFile() == null) { return; } if (myLanguageDescriptor.getLoadException() != null){ return; } try { DescriptorIO<LanguageDescriptor> io = new DescriptorIOFacade().standardProvider().languageDescriptorIO(); io.writeToFile(getModuleDescriptor(), getDescriptorFile()); } catch (Exception ex) { Logger.getLogger(getClass()).error("Save failed", ex); } } public List<SModel> getAccessoryModels() { List<SModel> result = new LinkedList<>(); for (SModelReference model : getModuleDescriptor().getAccessoryModels()) { SModel modelDescriptor = model.resolve(getRepository()); if (modelDescriptor != null) { result.add(modelDescriptor); } } return result; } public boolean isAccessoryModel(org.jetbrains.mps.openapi.model.SModelReference modelReference) { return myLanguageDescriptor.getAccessoryModels().stream().anyMatch(Predicate.isEqual(modelReference)); } public void removeAccessoryModel(org.jetbrains.mps.openapi.model.SModel sm) { // XXX why removal of accessory model is not done through ModuleDescriptor as other editing activities? // i.e. module properties add accessory models through MD, but remove them through Language, which is odd. final SModelReference accessoryModelRef = sm.getReference(); boolean changed = myLanguageDescriptor.getAccessoryModels().removeIf(accessoryModelRef::equals); if (changed) { // XXX Perhaps, setModuleDescriptor is too much, as it fires changed + dependenciesChange and eventually reloads classes, // while change in accessory models unlikely to affect any compiled class. // I'd stick to setChanged(true) + fireChanged() here, instead. setModuleDescriptor(myLanguageDescriptor); } } public String toString() { return getModuleName() + " [language]"; } @Deprecated(since = "3.3", forRemoval = true) //no full equivalent to this method, use appropriate method from LanguageAspectSupport private LanguageAspect getAspectForModel(@NotNull org.jetbrains.mps.openapi.model.SModel sm) { for (LanguageAspect la : LanguageAspect.values()) { if (la.get(this) == sm) { return la; } } return null; } public static Language getLanguageForLanguageAspect(org.jetbrains.mps.openapi.model.SModel modelDescriptor) { return getLanguageFor(modelDescriptor); } @Deprecated(since = "3.3", forRemoval = true) //no full equivalent to this method, use appropriate method from LanguageAspectSupport //no usages in MPS, 4 uses in mbeddr @Nullable public static LanguageAspect getModelAspect(org.jetbrains.mps.openapi.model.SModel sm) { if (sm == null) return null; SModule module = sm.getModule(); if (!(module instanceof Language)) { return null; } Language l = (Language) module; return l.getAspectForModel(sm); } public static boolean isLanguageOwnedAccessoryModel(org.jetbrains.mps.openapi.model.SModel sm) { SModule modelOwner = sm.getModule(); if (modelOwner instanceof Language) { Language l = (Language) modelOwner; return l.isAccessoryModel(sm.getReference()); } return false; } public static Language getLanguageFor(org.jetbrains.mps.openapi.model.SModel sm) { SModule owner = sm.getModule(); if (owner instanceof Language) { return (Language) owner; } return null; } // TODO // @Nullable // @Override // public Language clone(String targetRoot, String targetNamespace) { // LanguageDescriptor targetDescriptor = new LanguageDescriptor(); // IFile targetDescriptorFile = getFileSystem().getFile(targetRoot + File.separator + targetNamespace + MPSExtentions.DOT_LANGUAGE); // // targetDescriptor.setId(ModuleId.regular()); // targetDescriptor.setNamespace(targetNamespace); // getModuleDescriptor().cloneTo(targetDescriptor, PathConverters.forDescriptorFiles(targetDescriptorFile, getDescriptorFile())); // LanguageDescriptorPersistence.saveLanguageDescriptor(targetDescriptorFile, targetDescriptor, MacrosFactory.forModuleFile(targetDescriptorFile)); // // Language targetLanguage = new Language(targetDescriptor, targetDescriptorFile); // ModelRootCloneUtil.cloneModelRootsTo(getModelRoots(), targetLanguage); // // Iterator<Generator> targetGenerators = targetLanguage.getGenerators().iterator(); // for (Generator generator : getGenerators()) { // Generator targetGenerator = targetGenerators.next(); // ModelRootCloneUtil.cloneModelRootsTo(generator.getModelRoots(), targetGenerator); // } // // FIXME RADIMIR rename models here // // return targetLanguage; // } /*package*/ void register(@NotNull Generator generator) { assertCanChange(); myAttachedGenerators.add(generator); } /*package*/ void unregister(@NotNull Generator generator) { assertCanChange(); if (!myAttachedGenerators.remove(generator)) { throw new IllegalStateException(String.format("Generator %s has not been previously registered with the language %s", generator.getModuleName(), getModuleName())); } } public static class LanguageModelsAutoImports extends jetbrains.mps.project.ModelsAutoImportsManager.AutoImportsContributor { @Override public boolean isApplicable(SModule module) { return module instanceof Language; } @NotNull @Override public Collection<SLanguage> getLanguages(SModule contextModule, SModel model) { return LanguageAspectSupport.getMainLanguages(model); } @Override public Collection<SModuleReference> getDevKits(SModule contextModule, SModel forModel) { SModuleReference defaultDevkit = LanguageAspectSupport.getDefaultDevkit(forModel); if(defaultDevkit != null) { return Collections.singleton(defaultDevkit); } return Collections.singleton(BootstrapLanguages.getGeneralPurposeDevKit()); } } }