diff --git a/AGENTS.md b/AGENTS.md index b6685669..12913bba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,9 @@ [Verträge](docs/README.md) und [Modulgrenzen](docs/architecture.md). Veröffentlichungen und Arbeiten an Produktionssystemen brauchen einen konkreten Auftrag. -- Nutze [Jenesis 0.18.1 mit Java 25](docs/development/build.md) für alle elf - JVM-Module. POMs definieren Koordinaten und Abhängigkeiten. Der Gradle-Wrapper +- Nutze [Jenesis 0.18.1 mit Java 25](docs/development/build.md) für alle + [registrierten JVM-Module](build/interlis/JvmBuildInputs.java). POMs definieren + Koordinaten und Abhängigkeiten. Der Gradle-Wrapper baut die beiden Wasm-Backends mit explizitem geprüftem ConsumerSnapshot. `build/jenesis/`, `build/interlis/`, `build/InterlisJvmBuild.java` und der Upstreampin sind Quellen. Verwende die gezielten Clean-Sichten. diff --git a/docs/architecture.md b/docs/architecture.md index 94e654c1..029b51aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -59,6 +59,37 @@ Namen dienen der Sprachsemantik und ersetzen keine dauerhafte Identität. Fachliche Reihenfolgen liegen in geordneten Werten. Maps dienen als Indizes. Ein veröffentlichter Snapshot bleibt nach Schliessen des Workspace lesbar. +## Aufbau des semantischen Graphen + +`GraphBuilder` komponiert die lauflokalen Fachbinder und ruft sie in einer +festen Reihenfolge auf. Der Deklarationsindex enthält die Eingaben für die +Namensauflösung und den bedarfsweisen Aufbau. Die Deklarationsbindung +liefert bereits vorhandene Symbole und baut weitere Deklarationen bei +Bedarf auf. Die Typbindung kann einen Domänen- oder Attributtyp +reservieren und auflösen, bevor die zugehörigen Constraints gebunden +werden. Rekursionsmengen und fachliche Memos gehören den jeweiligen +Bindern. + +`SemanticBuildState` speichert den entstehenden Graphen. Seine lesende +Sicht ist intern und lauflokal. Typisierte Schreiboperationen unterscheiden +die Definition und Vervollständigung von Symbolen, Typ- und +Ausdrucksdaten sowie Basiskanten und effektive Member. Die Fachbinder +erhalten die zu ihrer Aufgabe passenden Schreiboperationen. Referenzen +verwenden eine gemeinsame Registrierung und Sequenz. Geordnete +Zwischenstände erhalten die ursprüngliche Reihenfolge und Herkunft. + +Namensbindung, Typbindung, Ausdrücke, Objektpfade, Constraints, +Vererbung und Übersetzung haben eigene Zuständigkeiten. Benannte +Kollaboratoren und Rückaufrufe verbinden die bedarfsweisen Operationen +in einem Lauf. Ein Arbeitsbudget zählt auch bereits verbrauchte +Overlayarbeit. Die Diagnostik schreibt in den laufgebundenen Collector. + +Die Übersetzungsbindung prüft und ergänzt die gebundenen Paare bis zum +Abschluss. Nach Transfernamespace und Predefined-Abschluss friert der +Speicher den Graphen ein. Die Coverage wird für den Lauf ermittelt. +`CompilationGate` verwendet Analyse, Diagnostik und Closure für die +einzige Entscheidung über die gültige Schemasicht. + ## Lauf und Hostgrenzen 1. Der Host erfasst Quellen und Konfiguration und beschafft fehlende @@ -71,6 +102,13 @@ Ein veröffentlichter Snapshot bleibt nach Schliessen des Workspace lesbar. 4. Der Host verwirft veraltete Ergebnisse vor Übernahme oder Veröffentlichung. Darstellung und Layout bleiben vom Compiler getrennt. +Im LSP-Host verwaltet `LspDocumentRegistry` die akzeptierten Dokumentstände +und `LspFileIdentityRegistry` die vom Dateisystem bestätigten Identitäten. +`LspInputReadiness` ordnet Dokumentoperationen und Anfragen während laufender +Dateiaufnahmen. `AnalysisScheduler` serialisiert Zustandsänderungen auf der +Owner-Queue und führt Compilerarbeit auf unveränderlichen Captures ausserhalb +der Queue aus. Diagnosen werden aus einer akzeptierten Quellsicht veröffentlicht. + Laufbeobachtung und Abbruch gelten pro Operation. Ein interner Fehler erzeugt einen eigenen Ergebniszustand. Projektoren weisen ihre Fähigkeiten explizit aus und melden ununterstützte Elemente, bevor Daten verloren gehen. Reine diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/AnalysisCoverageBuilder.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/AnalysisCoverageBuilder.java new file mode 100644 index 00000000..73222f82 --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/AnalysisCoverageBuilder.java @@ -0,0 +1,117 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.diagnostics.Diagnostic; +import guru.interlis.diagnostics.DiagnosticId; +import guru.interlis.diagnostics.Phase; +import guru.interlis.diagnostics.Severity; +import guru.interlis.execution.CancellationToken; +import guru.interlis.model.AnalysisCoverage; +import guru.interlis.model.CoverageStatus; +import guru.interlis.model.ErrorType; +import guru.interlis.source.SourceId; +import guru.interlis.source.SourceRange; +import guru.interlis.syntax.SyntaxCoverage; +import guru.interlis.syntax.SyntaxSnapshot; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Reports coverage of the requested analysis, using the same diagnostic selection as the snapshot. */ +final class AnalysisCoverageBuilder { + private static final List SEMANTIC_PHASES = List.of( + Phase.DECLARATION_INDEXING, Phase.BINDING, Phase.TYPE_CHECKING, Phase.SEMANTIC_CHECKING); + + private AnalysisCoverageBuilder() { } + + private record Location(Phase phase, Optional source) { } + + static AnalysisCoverage build(Closure closure, Set syntaxSources, + Map syntax, List diagnostics, + Set resolutionDiagnostics, GraphBuilder.GraphResult graph, + CancellationToken token) { + var modeledSources = new HashSet(); + closure.models().forEach(model -> modeledSources.add(model.sourceId())); + var typeFailures = new HashSet<>(graph.fragmentTypeFailures()); + for (var type : graph.graph().types()) { + token.throwIfCancellationRequested(); + if (type instanceof ErrorType error) typeFailures.addAll(error.diagnosticIds()); + } + + var blockers = new HashMap(); + var unsupportedSources = new HashSet(); + var unsupportedEntries = new ArrayList(); + for (var diagnostic : diagnostics) { + token.throwIfCancellationRequested(); + if (diagnostic.severity() != Severity.ERROR || resolutionDiagnostics.contains(diagnostic.id())) continue; + if (diagnostic.primaryRange().isPresent() && !closure.contains(diagnostic.primaryRange().orElseThrow())) continue; + var feature = graph.unsupportedFeatures().get(diagnostic.id()); + if (feature != null) { + unsupportedSources.add(feature.source()); + unsupportedEntries.add(new AnalysisCoverage.CoverageEntry(Phase.SEMANTIC_CHECKING, + Optional.of(feature.source()), CoverageStatus.NOT_IMPLEMENTED, + Optional.of(feature.feature()), Optional.of(diagnostic.id()))); + continue; + } + var source = diagnostic.primaryRange().map(SourceRange::source); + if (SEMANTIC_PHASES.contains(diagnostic.phase())) + blockers.putIfAbsent(new Location(diagnostic.phase(), source), diagnostic.id()); + // A generic semantic-rule or binding diagnostic may also have prevented an actual type result. + if (typeFailures.contains(diagnostic.id())) + blockers.putIfAbsent(new Location(Phase.TYPE_CHECKING, source), diagnostic.id()); + } + + var entries = new ArrayList(); + for (var source : syntaxSources) { + token.throwIfCancellationRequested(); + var snapshot = syntax.get(source); + if (snapshot == null) continue; + var coverage = snapshot.coverage(); + var location = Optional.of(source); + entries.add(entry(Phase.LEXING, location, syntaxStatus(snapshot, Phase.LEXING, coverage.lexicallyComplete()))); + boolean parsed = coverage.parseComplete() && coverage.consumedEntireInput(); + entries.add(entry(Phase.PARSING, location, syntaxStatus(snapshot, Phase.PARSING, parsed))); + if (!modeledSources.contains(source)) continue; + for (var phase : SEMANTIC_PHASES) { + var status = incompleteStatus(coverage); + if (status == CoverageStatus.COMPLETE) { + if (!parsed || !coverage.lexicallyComplete() || blockers.containsKey(new Location(phase, location))) + status = CoverageStatus.BLOCKED_BY_ERROR; + else if (phase == Phase.SEMANTIC_CHECKING && unsupportedSources.contains(source)) + status = CoverageStatus.NOT_IMPLEMENTED; + } + entries.add(entry(phase, location, status)); + } + } + for (var phase : SEMANTIC_PHASES) { + var diagnostic = blockers.get(new Location(phase, Optional.empty())); + if (diagnostic != null) + entries.add(new AnalysisCoverage.CoverageEntry(phase, Optional.empty(), CoverageStatus.BLOCKED_BY_ERROR, + Optional.empty(), Optional.of(diagnostic))); + } + entries.addAll(unsupportedEntries); + return new AnalysisCoverage(entries); + } + + private static CoverageStatus syntaxStatus(SyntaxSnapshot snapshot, Phase phase, boolean structurallyComplete) { + var status = incompleteStatus(snapshot.coverage()); + if (status != CoverageStatus.COMPLETE) return status; + boolean error = snapshot.diagnostics().stream() + .anyMatch(diagnostic -> diagnostic.severity() == Severity.ERROR && diagnostic.phase() == phase); + return structurallyComplete && !error ? CoverageStatus.COMPLETE : CoverageStatus.BLOCKED_BY_ERROR; + } + + private static CoverageStatus incompleteStatus(SyntaxCoverage coverage) { + if (coverage.cancelled()) return CoverageStatus.CANCELLED; + if (coverage.budgetExceeded()) return CoverageStatus.BUDGET_EXCEEDED; + return CoverageStatus.COMPLETE; + } + + private static AnalysisCoverage.CoverageEntry entry(Phase phase, Optional source, CoverageStatus status) { + return new AnalysisCoverage.CoverageEntry(phase, source, status, Optional.empty(), Optional.empty()); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/CompilationRun.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/CompilationRun.java index 19a130aa..abe55659 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/CompilationRun.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/CompilationRun.java @@ -8,8 +8,12 @@ import guru.interlis.compiler.CompilerServices; import guru.interlis.compiler.SourceSet; import guru.interlis.diagnostics.Diagnostic; +import guru.interlis.diagnostics.DiagnosticId; import guru.interlis.diagnostics.Phase; +import guru.interlis.execution.CancellationReason; import guru.interlis.execution.CancellationToken; +import guru.interlis.execution.OperationCancelledException; +import guru.interlis.execution.PhaseStatus; import guru.interlis.execution.RunLifecycle; import guru.interlis.model.SnapshotId; import guru.interlis.source.ContentFingerprint; @@ -26,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; /** One run-local compiler execution; mutable state is never published. */ final class CompilationRun { @@ -47,7 +52,7 @@ final class CompilationRun { RunOutput execute(ParseMode mode, RunLifecycle lifecycle) { SourceSet sourceSet = request.sources(); - verifySourceSet(sourceSet, lifecycle); + var resolutionDiagnostics = verifySourceSet(sourceSet, lifecycle); parseSources(sourceSet, mode, lifecycle); token.throwIfCancellationRequested(); IndexedDeclarations declarations = new DeclarationIndexer().index(syntax.values().stream().toList(), reuse); @@ -57,7 +62,9 @@ RunOutput execute(ParseMode mode, RunLifecycle lifecycle) { Closure closure; var indexPhase = lifecycle.beginPhase(Phase.DECLARATION_INDEXING, new guru.interlis.execution.RunEvent.PhaseMetadata(modelIndex.all().size())); + PhaseStatus indexStatus = PhaseStatus.FAILED; try { + token.throwIfCancellationRequested(); RootSelector rootSelector = new RootSelector(); roots = rootSelector.select(request.roots(), modelIndex, collector); ImportBinder importBinder = new ImportBinder(); @@ -66,8 +73,12 @@ RunOutput execute(ParseMode mode, RunLifecycle lifecycle) { closure = new Closure(List.of(), List.of(), java.util.Set.of(), List.of(), closure.diagnostics(), new java.util.LinkedHashSet<>(closure.reasons())); } + indexStatus = PhaseStatus.SUCCEEDED; + } catch (OperationCancelledException | AnalysisBudgetExceededException cancelled) { + indexStatus = PhaseStatus.CANCELLED; + throw cancelled; } finally { - indexPhase.finish(guru.interlis.execution.PhaseStatus.SUCCEEDED); + indexPhase.finish(indexStatus); } CompilationFingerprinter fingerprinter = new CompilationFingerprinter(); @@ -99,28 +110,39 @@ RunOutput execute(ParseMode mode, RunLifecycle lifecycle) { var bindingPhase = lifecycle.beginPhase(Phase.BINDING, new guru.interlis.execution.RunEvent.PhaseMetadata(declarations.all().size())); GraphBuilder.GraphResult graphResult; + PhaseStatus bindingStatus = PhaseStatus.FAILED; try { + token.throwIfCancellationRequested(); graphResult = builder.build(); + bindingStatus = PhaseStatus.SUCCEEDED; + } catch (OperationCancelledException | AnalysisBudgetExceededException cancelled) { + bindingStatus = PhaseStatus.CANCELLED; + throw cancelled; } finally { - bindingPhase.finish(guru.interlis.execution.PhaseStatus.SUCCEEDED); + bindingPhase.finish(bindingStatus); } token.throwIfCancellationRequested(); List diagnostics = collector.freeze(); + var syntaxSources = RequestDiagnosticSelector.syntaxSources(closure, request, token); + var requestDiagnostics = RequestDiagnosticSelector.select(diagnostics, closure, roots, syntaxSources, token); + var coverage = AnalysisCoverageBuilder.build(closure, syntaxSources, syntax, requestDiagnostics, + resolutionDiagnostics, graphResult, token); AnalysisProvenance provenance = new AnalysisProvenance(Fingerprints.COMPILER_VERSION, Fingerprints.GRAMMAR_VERSION, Fingerprints.SEMANTIC_PROFILE, sourceSet.fingerprint().value(), request.options().fingerprint(), sourceFingerprints(sourceSet)); AnalysisSnapshot analysis = new AnalysisSnapshot(snapshotId, graphResult.graph(), - revisions(sourceSet), syntax, diagnostics, - RequestDiagnosticSelector.select(diagnostics, closure, roots, request, token), graphResult.coverage(), + revisions(sourceSet), syntax, diagnostics, requestDiagnostics, coverage, fingerprint, provenance); return new RunOutput(analysis, closure, roots, graphResult); } - private void verifySourceSet(SourceSet sourceSet, RunLifecycle lifecycle) { + private Set verifySourceSet(SourceSet sourceSet, RunLifecycle lifecycle) { var phase = lifecycle.beginPhase(Phase.SOURCE_RESOLUTION, new guru.interlis.execution.RunEvent.PhaseMetadata(sourceSet.sources().size())); + PhaseStatus status = PhaseStatus.FAILED; try { + token.throwIfCancellationRequested(); long total = 0; for (SourceText text : sourceSet.sources().values()) { total = Math.addExact(total, text.length()); @@ -130,16 +152,23 @@ private void verifySourceSet(SourceSet sourceSet, RunLifecycle lifecycle) { request.options().budget().maxTotalSourceUtf16Length()); } new SourceSetVerifier().verify(sourceSet, collector); - collector.importAll(sourceSet.resolutionDiagnostics()); + var importedDiagnostics = Set.copyOf(collector.importAll(sourceSet.resolutionDiagnostics()).values()); + status = PhaseStatus.SUCCEEDED; + return importedDiagnostics; + } catch (OperationCancelledException | AnalysisBudgetExceededException cancelled) { + status = PhaseStatus.CANCELLED; + throw cancelled; } finally { - phase.finish(guru.interlis.execution.PhaseStatus.SUCCEEDED); + phase.finish(status); } } private void parseSources(SourceSet sourceSet, ParseMode mode, RunLifecycle lifecycle) { var phase = lifecycle.beginPhase(Phase.LEXING, new guru.interlis.execution.RunEvent.PhaseMetadata(sourceSet.sources().size())); + PhaseStatus status = PhaseStatus.FAILED; try { + token.throwIfCancellationRequested(); List ids = new ArrayList<>(sourceSet.sources().keySet()); ids.sort(Comparator.comparing(SourceId::value)); for (SourceId id : ids) { @@ -158,8 +187,8 @@ private void parseSources(SourceSet sourceSet, ParseMode mode, RunLifecycle life new ParseOptions(mode, java.util.Optional.empty(), request.options().budget()), token); } - if(snapshot.coverage().budgetExceeded())throw new guru.interlis.execution.OperationCancelledException( - guru.interlis.execution.CancellationReason.RESOURCE_LIMIT); + if (snapshot.coverage().budgetExceeded()) + throw new OperationCancelledException(CancellationReason.RESOURCE_LIMIT); syntax.put(id, snapshot); collector.importAll(snapshot.diagnostics()); } @@ -168,8 +197,12 @@ private void parseSources(SourceSet sourceSet, ParseMode mode, RunLifecycle life throw new AnalysisBudgetExceededException("maxTokens", request.options().budget().maxTokens()); } + status = PhaseStatus.SUCCEEDED; + } catch (OperationCancelledException | AnalysisBudgetExceededException cancelled) { + status = PhaseStatus.CANCELLED; + throw cancelled; } finally { - phase.finish(guru.interlis.execution.PhaseStatus.SUCCEEDED); + phase.finish(status); } } diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ConstraintBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ConstraintBindings.java new file mode 100644 index 00000000..a2c7bdfe --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ConstraintBindings.java @@ -0,0 +1,370 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.compiler.internal.ExpressionBindings.ExpressionResult; +import guru.interlis.model.AssociationSymbol; +import guru.interlis.model.ClassSymbol; +import guru.interlis.model.CollectionType; +import guru.interlis.model.ConstraintDefinition; +import guru.interlis.model.ConstraintSymbol; +import guru.interlis.model.CoordinateType; +import guru.interlis.model.DomainConstraint; +import guru.interlis.model.EnumerationType; +import guru.interlis.model.EnumerationValue; +import guru.interlis.model.ExpressionId; +import guru.interlis.model.FormattedType; +import guru.interlis.model.FunctionSymbol; +import guru.interlis.model.GeometryType; +import guru.interlis.model.MandatoryConstraint; +import guru.interlis.model.NumericRange; +import guru.interlis.model.NumericType; +import guru.interlis.model.PlausibilityConstraint; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.StructureSymbol; +import guru.interlis.model.StructureValueType; +import guru.interlis.model.SymbolHeader; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TextType; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; +import guru.interlis.model.TypedExpression; +import guru.interlis.model.UniquenessConstraint; +import guru.interlis.model.ViewSymbol; +import guru.interlis.source.SourceRange; +import guru.interlis.syntax.ConstraintDeclarationSyntax; +import guru.interlis.syntax.ExpressionSyntax; +import guru.interlis.syntax.ExternalConstraintSyntax; +import guru.interlis.syntax.PathExpressionSyntax; +import guru.interlis.syntax.TopicSyntax; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Binds typed constraint definitions in their actual object context. */ +final class ConstraintBindings { + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameResolver resolver; + private final Supplier paths; + private final InheritanceBindings inheritance; + private final ReferenceBindings references; + private final Supplier expressions; + private final Supplier typeBindings; + private final StructureBindings structures; + private final Function symbolHeader; + private final Map externalConstraintTargets = new LinkedHashMap<>(); + + ConstraintBindings(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameResolver resolver, + Supplier paths, + InheritanceBindings inheritance, + ReferenceBindings references, + Supplier expressions, + Supplier typeBindings, + StructureBindings structures, + Function symbolHeader) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.resolver = resolver; + this.paths = paths; + this.inheritance = inheritance; + this.references = references; + this.expressions = expressions; + this.typeBindings = typeBindings; + this.structures = structures; + this.symbolHeader = symbolHeader; + } + + /** Named constraints share the declaring viewable's local namespace, including + * same-topic CONSTRAINTS OF blocks. Inherited-topic contexts remain distinct. */ + void validateConstraintNames() { + record ConstraintScope(SymbolId target, SymbolId topic) {} + Map> names = new LinkedHashMap<>(); + var declarations = index.all().stream() + .filter(d -> d.kind() == SymbolKind.CONSTRAINT && d.language() == guru.interlis.model.LanguageVersion.ILI24) + .sorted(Comparator.comparing((DeclInfo d) -> d.sourceId().value()) + .thenComparingInt(d -> d.syntax().range().startOffset())).toList(); + for (var declaration : declarations) { + work.checkpoint(); + var syntax = declaration.syntax() instanceof ExternalConstraintSyntax external + ? external.constraint() : (ConstraintDeclarationSyntax) declaration.syntax(); + if (syntax.name().isEmpty()) continue; + SymbolId target = externalConstraintTargets.getOrDefault(declaration.id(), declaration.owner().orElse(null)); + var symbol = state.symbol(target); + if (!(symbol instanceof ClassSymbol || symbol instanceof StructureSymbol + || symbol instanceof AssociationSymbol || symbol instanceof ViewSymbol)) continue; + var scope = new ConstraintScope(target, paths.get().scopeRoot(declaration.id())); + if (!names.computeIfAbsent(scope, ignored -> new LinkedHashSet<>()).add(syntax.name().orElseThrow())) + diagnostics.emitRule(declaration, "constraint names must be unique within their declaring class", + "CONSTRAINT_NAME", declaration.nameRange().orElse(syntax.range())); + } + } + + ConstraintSymbol buildConstraint(DeclInfo decl) { + ConstraintDeclarationSyntax syntax; + Optional externalTarget = Optional.empty(); + if (decl.syntax() instanceof ExternalConstraintSyntax external) { + syntax = external.constraint(); + var targetKinds = Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION); + var resolution = resolver.resolve(decl.id(), external.target(), targetKinds); + externalTarget = resolution.target(); + if (resolution.status() == NameResolver.Status.UNRESOLVED && external.target().segments().size() == 1) { + externalTarget = inheritance.baseTopicMember(decl.id(), external.target().text(), targetKinds); + } + if (externalTarget.isEmpty()) { + diagnostics.unresolved(external.target()); + if (resolution.status() == NameResolver.Status.AMBIGUOUS) { + references.recordResolution(decl, external.target(), resolution, ReferenceUsage.EXPRESSION); + } + } else { + SymbolId target = externalTarget.orElseThrow(); + references.recordReference(decl, external.target(), target, ReferenceUsage.EXPRESSION); + boolean inherited = inheritance.baseTopicMember(decl.id(), + index.byId(target).orElseThrow().name(), + Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION)).filter(target::equals).isPresent(); + if (!Objects.equals(paths.get().scopeRoot(decl.id()), paths.get().scopeRoot(target)) && !inherited) { + diagnostics.emitRule(decl, "external constraints must be declared in the target's topic", + "CONSTRAINT_TARGET_TOPIC"); + } + } + } else { + syntax = (ConstraintDeclarationSyntax) decl.syntax(); + } + externalTarget.ifPresent(target -> externalConstraintTargets.put(decl.id(), target)); + SymbolId context = paths.get().contextualObject(expressions.get().expressionContext(decl.id()), paths.get().scopeRoot(decl.id())); + if (syntax.existence().isPresent()) { + var existence = syntax.existence().orElseThrow(); + ExpressionResult source = constraintPath(existence.source(), decl, context, false); + requireConstraintValue(decl, source, false); + List targets = new ArrayList<>(); + for (var target : existence.targets()) { + work.checkpoint(); + var targetKinds = Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION, SymbolKind.VIEW); + var resolution = resolver.resolve(decl.id(), target.viewable(), targetKinds, false); + var resolved = resolution.target(); + if (resolution.status() == NameResolver.Status.UNRESOLVED && target.viewable().segments().size() == 1) { + resolved = inheritance.baseTopicMember(decl.id(), target.viewable().text(), targetKinds); + } + if (resolved.isEmpty()) { + references.recordResolution(decl, target.viewable(), resolution, ReferenceUsage.EXPRESSION); + diagnostics.unresolved(target.viewable()); + continue; + } + SymbolId targetScope = resolved.orElseThrow(); + references.recordReference(decl, target.viewable(), targetScope, ReferenceUsage.EXPRESSION); + SymbolId ownerTopic = paths.get().scopeRoot(decl.id()); + SymbolId targetTopic = paths.get().scopeRoot(targetScope); + boolean depends = ownerTopic == null || targetTopic == null + || index.byId(ownerTopic).map(d -> d.kind() != SymbolKind.TOPIC).orElse(true) + || index.byId(targetTopic).map(d -> d.kind() != SymbolKind.TOPIC).orElse(true) + || paths.get().topicAvailable(ownerTopic, targetTopic) + || inheritance.baseChain(ownerTopic).stream().map(index::byId).flatMap(Optional::stream) + .filter(d -> d.syntax() instanceof TopicSyntax) + .anyMatch(d -> ((TopicSyntax) d.syntax()).dependencies().stream() + .anyMatch(n -> resolver.resolve(d.id(), n, Set.of(SymbolKind.TOPIC)).target() + .filter(targetTopic::equals).isPresent())); + if (!depends) diagnostics.emitRule(decl, "EXISTENCE target topic requires DEPENDS ON", "EXISTENCE_DEPENDS"); + ExpressionResult path = constraintPath(target.path(), decl, targetScope, false); + requireConstraintValue(decl, path, false); + if (!source.error() && !path.error() && !existenceCompatible(source.typeId(), path.typeId())) { + TypeDescriptor from = typeBindings.get().effectiveDescriptor(source.typeId()), to = typeBindings.get().effectiveDescriptor(path.typeId()); + boolean familyPending = from.kind() == to.kind() && switch (from.kind()) { + case GENERIC, DEFERRED, PENDING -> true; + default -> false; + }; + if (familyPending) diagnostics.emitUnsupported(decl.sourceId(), target.path().range(), + "EXISTENCE_TYPE_COMPATIBILITY", "EXISTENCE compatibility for " + from.kind()); + else diagnostics.emitTypeMismatch(decl.sourceId(), target.path().range(), typeBindings.get().typeName(source.typeId()), typeBindings.get().typeName(path.typeId())); + } + targets.add(new guru.interlis.model.ExistenceConstraint.ExistenceTarget(targetScope, path.id())); + } + return new ConstraintSymbol(symbolHeader.apply(decl), new guru.interlis.model.ExistenceConstraint( + syntax.name(), source.id(), targets), externalTarget); + } + if (syntax.constraintKind() == guru.interlis.syntax.ConstraintKind.SET + && decl.owner().flatMap(index::byId).filter(d -> d.kind() == SymbolKind.STRUCTURE).isPresent()) + diagnostics.emitRule(decl, "SET CONSTRAINT is not allowed in a structure", "SET_IN_STRUCTURE"); + if (syntax.constraintKind() == guru.interlis.syntax.ConstraintKind.UNIQUENESS) { + Optional where = syntax.expression().map(e -> { + ExpressionResult condition = expressions.get().buildExpression(e, decl.id()); + if (!condition.error() && !typeBindings.get().isBoolean(condition.typeId())) + diagnostics.emitTypeMismatch(decl.sourceId(), e.range(), typeBindings.get().typeName(condition.typeId()), "BOOLEAN"); + return condition.id(); + }); + List keys = new ArrayList<>(); + List localKeys = new ArrayList<>(); + if (syntax.localPrefix().isPresent()) { + if (syntax.basket()) diagnostics.emitRule(decl, "UNIQUE cannot combine BASKET and LOCAL", "UNIQUE_KEY"); + ExpressionResult prefix = constraintPath(syntax.localPrefix().orElseThrow(), decl, context, true); + SymbolId structure = structures.target(typeBindings.get().effectiveDescriptor(prefix.typeId())); + if (!prefix.error() && structure == null) + diagnostics.emitRule(decl, "LOCAL prefix must select a structure attribute", "UNIQUE_LOCAL_PREFIX"); + for (var key : syntax.uniqueKeys()) { + ExpressionResult field = constraintPath(key, decl, structure, false); + requireConstraintValue(decl, field, true); + keys.add(field.id()); + } + localKeys.add(new UniquenessConstraint.LocalUniqueness(prefix.id(), keys)); + keys = List.of(); + } else { + if (context != null && index.byId(context).filter(d -> d.kind() == SymbolKind.STRUCTURE).isPresent()) + diagnostics.emitRule(decl, "global UNIQUE requires identifiable objects", "UNIQUE_STRUCTURE"); + for (var key : syntax.uniqueKeys()) { + ExpressionResult path = constraintPath(key, decl, context, false); + requireConstraintValue(decl, path, true); + keys.add(path.id()); + } + } + return new ConstraintSymbol(symbolHeader.apply(decl), new UniquenessConstraint(syntax.name(), + syntax.basket(), where, keys, localKeys), externalTarget); + } + Optional setWhere=syntax.where().map(e->{ + var condition=expressions.get().buildExpression(e,decl.id()); + if(!condition.error() && !typeBindings.get().isBoolean(condition.typeId()))diagnostics.emitTypeMismatch(decl.sourceId(),e.range(),typeBindings.get().typeName(condition.typeId()),"BOOLEAN"); + return condition.id(); + }); + if(syntax.constraintKind()==guru.interlis.syntax.ConstraintKind.PLAUSIBILITY && syntax.plausibility().isEmpty()) { + var error=missingConstraintExpression(decl,syntax.range()); + return new ConstraintSymbol(symbolHeader.apply(decl),new MandatoryConstraint(syntax.name(),error.id()),externalTarget); + } + SymbolId expressionOwner = decl.id(); + ExpressionResult expression = syntax.expression() + .map(e -> expressions.get().buildExpression(e, expressionOwner)) + .orElseGet(() -> missingConstraintExpression(decl,syntax.range())); + if (!expression.error() && !typeBindings.get().isBoolean(expression.typeId())) { + diagnostics.emitTypeMismatch(decl.sourceId(), syntax.range(), typeBindings.get().typeName(expression.typeId()), + "BOOLEAN"); + } + if (!expression.error() && typeBindings.get().isBoolean(expression.typeId()) && syntax.constraintKind()==guru.interlis.syntax.ConstraintKind.SET + && decl.language()==guru.interlis.model.LanguageVersion.ILI24 && !hasObjectSetFunction(expression.id())) { + var cause=diagnostics.emitTypeMismatch(decl.sourceId(),state.expression(expression.id()).range(), + "SET condition without OBJECTS function","SET condition with OBJECTS function"); + expression=expressions.get().failedFunctionArgument(state.expression(expression.id()).range(),List.of(cause)); + } + ConstraintDefinition definition = switch (syntax.constraintKind()) { + case MANDATORY -> new MandatoryConstraint(syntax.name(), expression.id()); + case PLAUSIBILITY -> new PlausibilityConstraint(syntax.name(), + syntax.plausibility().orElseThrow().percentage().exactValue(), syntax.plausibility().orElseThrow().comparison(), + expression.id()); + case DOMAIN -> new DomainConstraint(expression.id()); + case SET -> new guru.interlis.model.SetConstraint(syntax.name(), + syntax.basket()?guru.interlis.model.SetConstraint.SetConstraintScope.BASKET: guru.interlis.model.SetConstraint.SetConstraintScope.GLOBAL, setWhere, + expression.id()); + default -> new MandatoryConstraint(syntax.name(), expression.id()); + }; + return new ConstraintSymbol(symbolHeader.apply(decl), definition, externalTarget); + } + + /** 2.4 requires an actual call with an OBJECTS formal somewhere in the set + * condition; an ALL value or such a call only in WHERE does not suffice. */ + private boolean hasObjectSetFunction(ExpressionId root) { + var pending=new java.util.ArrayDeque();var seen=new java.util.HashSet();pending.add(root); + while(!pending.isEmpty()) { + work.checkpoint();var id=pending.removeFirst();if(!seen.add(id))continue;var expression=state.expression(id); + if(expression instanceof guru.interlis.model.FunctionCallExpression call + && call.targetFunction().map(state::symbol).filter(FunctionSymbol.class::isInstance).map(FunctionSymbol.class::cast) + .stream().flatMap(f->f.parameters().stream()).map(p->typeBindings.get().effectiveDescriptor(p.typeId())) + .anyMatch(t->t instanceof guru.interlis.model.ObjectArgumentType objects&&objects.objects()))return true; + pending.addAll(expression.children()); + } + return false; + } + + private ExpressionResult constraintPath(ExpressionSyntax syntax, DeclInfo owner, + SymbolId start, boolean localPrefix) { + if (syntax instanceof PathExpressionSyntax path) + return paths.get().pathExpression(expressions.get().nextExpressionId(), path, owner.id(), start, localPrefix); + return expressions.get().buildExpression(syntax, owner.id()); // Unsupported operators keep their explicit diagnostic. + } + + private void requireConstraintValue(DeclInfo owner, ExpressionResult result, boolean unique) { + if (result.error()) return; + TypedExpression expression = state.expression(result.id()); + TypeDescriptor type = typeBindings.get().effectiveDescriptor(result.typeId()); + boolean invalid = unique && type instanceof CollectionType || type instanceof guru.interlis.model.ObjectArgumentType o && o.objects(); + if (unique) invalid |= type instanceof StructureValueType; + else invalid |= !(expression instanceof guru.interlis.model.PathExpression p + && (p.path().steps().getLast() instanceof guru.interlis.model.PathStep.AttributeStep + || p.path().steps().getLast() instanceof guru.interlis.model.PathStep.IndexStep)); + if (invalid) diagnostics.emitRule(owner, unique ? "UNIQUE key must be a singular non-structure value" + : "EXISTENCE path must select one attribute value", unique ? "UNIQUE_KEY" : "EXISTENCE_PATH"); + } + + private boolean existenceCompatible(TypeId source, TypeId target) { + TypeDescriptor actual = typeBindings.get().effectiveDescriptor(source); + TypeDescriptor expected = typeBindings.get().effectiveDescriptor(target); + if (actual instanceof CollectionType a && expected instanceof CollectionType b) + return a.collectionKind() == b.collectionKind() && existenceCompatible(a.elementType(), b.elementType()); + if (actual instanceof CollectionType a && !paths.get().isMany(a.cardinality())) actual = typeBindings.get().effectiveDescriptor(a.elementType()); + if (expected instanceof CollectionType b && !paths.get().isMany(b.cardinality())) expected = typeBindings.get().effectiveDescriptor(b.elementType()); + if (actual == null || expected == null) return false; + if (actual instanceof guru.interlis.model.OidType a && expected instanceof guru.interlis.model.OidType b) + return a.oidKind() == guru.interlis.model.OidKind.ANY || b.oidKind() == guru.interlis.model.OidKind.ANY + || a.oidKind() == b.oidKind() && (a.valueType().isEmpty() && b.valueType().isEmpty() + || a.valueType().isPresent() && b.valueType().isPresent() + && existenceCompatible(a.valueType().orElseThrow(), b.valueType().orElseThrow())); + if (actual.id().equals(expected.id())) return true; + if (actual instanceof GeometryType geometry && expected instanceof CoordinateType) { + actual = geometry.vertexDomain().map(state::declaredType).map(typeBindings.get()::effectiveDescriptor).orElse(null); + } + if (actual instanceof NumericType a && expected instanceof NumericType b) + return numericValueCompatible(a.range(), b.range()); + if (actual instanceof TextType && expected instanceof TextType) return true; + if (actual instanceof CoordinateType a && expected instanceof CoordinateType b) { + if (a.dimensions() != b.dimensions() || a.multiCoordinate() != b.multiCoordinate() + || !a.referenceSystem().equals(b.referenceSystem())) return false; + for (int i = 0; i < a.dimensions(); i++) + if (!numericValueCompatible(a.axes().get(i), b.axes().get(i))) return false; + return true; + } + if (actual instanceof guru.interlis.model.AlignmentType a && expected instanceof guru.interlis.model.AlignmentType b) + return a.alignment() == b.alignment(); + if (actual instanceof guru.interlis.model.BlackboxType a && expected instanceof guru.interlis.model.BlackboxType b) + return a.blackboxKind() == b.blackboxKind(); + if (actual instanceof FormattedType a && expected instanceof FormattedType b) { + if (a.predefined().isPresent() || b.predefined().isPresent()) return a.predefined().equals(b.predefined()); + return a.structure().equals(b.structure()) && a.parts().stream().map(p -> List.of(p.attribute(), p.width(), p.format(), p.literal())).toList() + .equals(b.parts().stream().map(p -> List.of(p.attribute(), p.width(), p.format(), p.literal())).toList()); + } + if (actual instanceof EnumerationType a && expected instanceof EnumerationType b) + return guru.interlis.model.internal.TypeCompatibility.relatedEnumerations(a, b, typeBindings.get()::effectiveDescriptor) || a.ordered() == b.ordered() && a.circular() == b.circular() + && a.values().stream().map(EnumerationValue::path).toList() + .equals(b.values().stream().map(EnumerationValue::path).toList()); + return guru.interlis.model.internal.TypeCompatibility.assign(actual, expected, + typeBindings.get()::effectiveDescriptor, state::baseOf, domain -> typeBindings.get().effectiveDescriptor(state.declaredType(domain))).decision() == guru.interlis.model.RelationDecision.ALLOWED; + } + + private boolean numericValueCompatible(NumericRange a, NumericRange b) { + return a.unit().equals(b.unit()) && a.referenceSystem().equals(b.referenceSystem()) + && a.direction() == b.direction() && a.circular() == b.circular(); + } + + /** A missing parsed condition is a source recovery error, never an implementation gap. */ + private ExpressionResult missingConstraintExpression(DeclInfo owner,SourceRange range) { + var causes=diagnostics.syntaxErrors().stream().filter(d->d.primaryRange().filter(r->r.source().equals(range.source()) + && r.revision().equals(range.revision()) && r.startOffset()<=range.endOffset() && r.endOffset()>=range.startOffset()).isPresent()) + .map(guru.interlis.diagnostics.Diagnostic::id).toList(); + if(causes.isEmpty())causes=List.of(diagnostics.emitRule(owner,"constraint syntax is incomplete","SOURCE_MISSING_CONSTRAINT",range)); + return expressions.get().failedValueExpression(expressions.get().nextExpressionId(),range,causes); + } + + SymbolId externalConstraintTarget(SymbolId constraint, SymbolId fallback) { + return externalConstraintTargets.getOrDefault(constraint, fallback); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/DeclarationBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/DeclarationBindings.java new file mode 100644 index 00000000..620392cc --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/DeclarationBindings.java @@ -0,0 +1,911 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.diagnostics.DiagnosticArgument; +import guru.interlis.diagnostics.DiagnosticId; +import guru.interlis.model.AssociationSymbol; +import guru.interlis.model.AttributeSymbol; +import guru.interlis.model.Cardinality; +import guru.interlis.model.ClassSymbol; +import guru.interlis.model.GeometryType; +import guru.interlis.model.StructureValueType; +import guru.interlis.model.CollectionType; +import guru.interlis.model.DeclaredName; +import guru.interlis.model.Documentation; +import guru.interlis.model.DomainSymbol; +import guru.interlis.model.EnumElementSymbol; +import guru.interlis.model.EnumerationType; +import guru.interlis.model.EnumerationValue; +import guru.interlis.model.ExpressionId; +import guru.interlis.model.MetaAttributeSet; +import guru.interlis.model.ModelImport; +import guru.interlis.model.ModelSymbol; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceType; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.RoleSymbol; +import guru.interlis.model.StructureSymbol; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolHeader; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TopicSymbol; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; +import guru.interlis.model.FunctionSymbol; +import guru.interlis.model.FunctionParameter; +import guru.interlis.model.UnsupportedSymbol; +import guru.interlis.syntax.AssociationDeclarationSyntax; +import guru.interlis.syntax.AttributeDeclarationSyntax; +import guru.interlis.syntax.ClassDeclarationSyntax; +import guru.interlis.syntax.FunctionDeclarationSyntax; +import guru.interlis.syntax.RoleDeclarationSyntax; +import guru.interlis.syntax.StructureDeclarationSyntax; +import guru.interlis.syntax.TopicSyntax; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** Builds indexed declarations on demand and completes model metadata. */ +final class DeclarationBindings { + private final SemanticGraphRead.Symbols state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameResolver resolver; + private final Closure closure; + private final OverlayBindings overlays; + private final UnitBindings units; + private final LineForms lineForms; + private final MetadataBindings metadata; + private final GenericBindings generics; + private final ConstraintBindings constraints; + private final GraphicBuilder graphics; + private final ViewBuilder views; + private final NameBindings names; + private final InheritanceBindings inheritance; + private final ReferenceBindings references; + private final TypeBindings typeBindings; + private final ExpressionBindings expressions; + private final Map> provisionalSymbols = new HashMap<>(); + private final Set buildingAssociations = new HashSet<>(); + private final Map modelNames = new LinkedHashMap<>(); + + DeclarationBindings(SemanticGraphRead.Symbols state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameResolver resolver, + Closure closure, + OverlayBindings overlays, + UnitBindings units, + LineForms lineForms, + MetadataBindings metadata, + GenericBindings generics, + ConstraintBindings constraints, + GraphicBuilder graphics, + ViewBuilder views, + NameBindings names, + InheritanceBindings inheritance, + ReferenceBindings references, + TypeBindings typeBindings, + ExpressionBindings expressions) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.resolver = resolver; + this.closure = closure; + this.overlays = overlays; + this.units = units; + this.lineForms = lineForms; + this.metadata = metadata; + this.generics = generics; + this.constraints = constraints; + this.graphics = graphics; + this.views = views; + this.names = names; + this.inheritance = inheritance; + this.references = references; + this.typeBindings = typeBindings; + this.expressions = expressions; + } + + void validateTypeNamespaces() { + var typeKinds = DeclarationNamespaces.TYPE_KINDS; + var names = new LinkedHashMap>(); + var declarations = index.all().stream().sorted(Comparator.comparingInt(DeclInfo::order)).toList(); + for (var declaration : declarations) { + work.checkpoint(); + if (declaration.owner().isPresent() && declaration.nameRange().isPresent() + && typeKinds.contains(declaration.kind())) { + var scope = names.computeIfAbsent(declaration.owner().orElseThrow(), ignored -> new LinkedHashMap<>()); + var prior = scope.putIfAbsent(declaration.name(), declaration); + if (prior != null) emitNamespaceRule(declaration, prior, + "type names must be unique in their declaring scope", "SCOPE_TYPE_NAME_UNIQUE"); + } + } + for (var declaration : declarations) { + work.checkpoint(); + if (declaration.owner().isEmpty() || declaration.nameRange().isEmpty() + || !typeKinds.contains(declaration.kind())) continue; + var owner = declaration.owner().orElseThrow(); + var outer = index.byId(owner).flatMap(DeclInfo::owner); + while (outer.isPresent()) { + work.checkpoint(); + var prior = names.getOrDefault(outer.orElseThrow(), Map.of()).get(declaration.name()); + if (prior != null) { + emitNamespaceRule(declaration, prior, + "local type names must not shadow an enclosing type name", "SCOPE_OUTER_TYPE_NAME"); + break; + } + outer = index.byId(outer.orElseThrow()).flatMap(DeclInfo::owner); + } + if (declaration.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) continue; + var seen = new HashSet(); + for (var inherited = state.baseOf(owner); inherited != null && seen.add(inherited); inherited = state.baseOf(inherited)) { + work.checkpoint(); + var prior = names.getOrDefault(inherited, Map.of()).get(declaration.name()); + if (prior != null) { + emitNamespaceRule(declaration, prior, + "inherited type names require an explicit EXTENDED declaration", "SCOPE_INHERITED_TYPE_NAME"); + break; + } + } + } + } + + private void emitNamespaceRule(DeclInfo declaration, DeclInfo prior, String rule, String reason) { + var related = new guru.interlis.diagnostics.RelatedLocation(prior.nameRange().orElseThrow(), + "diagnostic.related.conflictingDeclaration", + List.of(new DiagnosticArgument.IdentifierArgument("name", prior.name()))); + diagnostics.emitRule(declaration, rule, reason, List.of(related)); + } + + void buildModel(IndexedDeclarations.ModelEntry entry) { + DeclInfo decl = entry.declaration(); + guru.interlis.syntax.ModelSyntax modelSyntax = entry.model().syntax(); + try { + String issuer = modelSyntax.issuer(); + if (issuer.length() > 1023 || issuer.chars().anyMatch(c -> c > 127)) + throw new java.net.URISyntaxException(issuer, "issuer outside URI character/length profile"); + new java.net.URI(issuer); + } catch (java.net.URISyntaxException invalid) { + diagnostics.emitRule(decl, "model issuer must be an ASCII URI reference of at most 1023 characters", "MODEL_ISSUER"); + } + if (modelSyntax.endName().isPresent() + && !modelSyntax.endName().orElseThrow().equals(modelSyntax.name())) { + diagnostics.emitRule(decl, "MODEL end name does not match declaration", "MODEL_END_NAME"); + } + if (modelSyntax.modelKind() == guru.interlis.model.ModelKind.SYMBOLOGY + && modelSyntax.languageVersion() == guru.interlis.model.LanguageVersion.ILI23 && !modelSyntax.contracted()) + diagnostics.emitRule(decl, "SYMBOLOGY MODEL requires CONTRACTED in INTERLIS 2.3", "SYMBOLOGY_CONTRACTED"); + if (modelSyntax.modelKind() == guru.interlis.model.ModelKind.TYPE) { + for (var child : index.children(decl.id())) { + work.checkpoint(); + if (Set.of(SymbolKind.TOPIC, SymbolKind.PARAMETER).contains(child.kind()) + || child.kind() == SymbolKind.METADATA_BASKET + && (!(child.syntax() instanceof guru.interlis.syntax.MetadataBasketDeclarationSyntax basket) + || basket.basketKind()!=guru.interlis.model.MetadataBasketSymbol.Kind.REFSYSTEM) + || child.kind() == SymbolKind.CLASS && !child.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) + diagnostics.emitRule(child, "TYPE MODEL may only declare type definitions", "TYPE_MODEL_CONTENT"); + } + } + // The inventory may retain several versions. Uniqueness belongs to the + // selected closure, not to every available source in the workspace. + if (closure.models().contains(entry.model())) { + DeclInfo previousModel = modelNames.putIfAbsent(modelSyntax.name(), decl); + if (previousModel != null && !previousModel.id().equals(decl.id())) { + diagnostics.emitRule(decl, "model name must be unique in the selected closure", "MODEL_NAME_UNIQUE"); + } + } + Set parameterNames = new LinkedHashSet<>(); + for (var child : index.children(decl.id())) { + if (child.kind() == SymbolKind.PARAMETER && !parameterNames.add(child.name())) + diagnostics.emitRule(child, "runtime parameter name must be unique in its model", "PARAMETER_NAME_UNIQUE"); + } + Set modelLevelNames = new LinkedHashSet<>(); + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + if (child.kind() == SymbolKind.DOMAIN || child.kind() == SymbolKind.TOPIC) { + if (!modelLevelNames.add(child.name())) { + diagnostics.emitRule(child, "topic/domain name must be unique in a model", + "MODEL_MEMBER_NAME_UNIQUE"); + } + } + if (child.kind() == SymbolKind.TOPIC) { + Set classNames = new LinkedHashSet<>(); + for (DeclInfo topicChild : index.children(child.id())) { + work.checkpoint(); + if (topicChild.kind() == SymbolKind.CLASS + && !classNames.add(topicChild.name())) { + diagnostics.emitRule(topicChild, "class name must be unique in a topic", + "TOPIC_CLASS_NAME_UNIQUE"); + } + } + } + } + List members = index.children(decl.id()).stream().map(DeclInfo::id).toList(); + List imports = new ArrayList<>(); + for (guru.interlis.syntax.ImportSyntax site : entry.model().syntax().imports()) { + work.checkpoint(); + SymbolId target = null; + for (Closure.ImportEdge edge : closure.importEdges()) { + work.checkpoint(); + if (edge.importer().sourceId().equals(entry.model().sourceId()) + && edge.importer().declarationOffset() == entry.model().declarationOffset() + && edge.site().range().equals(site.range())) { + target = index.findModelId(edge.target().name(), edge.target().declarationOffset(), + edge.target().sourceId()).orElse(null); + break; + } + } + if (site.modelName().equals("INTERLIS")) target = PredefinedModels.ensure(state, entry.declaration().language()); + imports.add(new ModelImport(site.modelName(), site.unqualified(), site.range(), + Optional.ofNullable(target))); + } + var modelOptions = entry.model().syntax().options(); + modelOptions.characterSet().ifPresent(option -> { + if (CharacterSets.canonical(option.value()).isEmpty()) diagnostics.emitRule(decl, "CHARSET name is not registered by IANA", "MODEL_CHARSET", ((Origin.DeclaredOrigin)option.origin()).range()); + }); + modelOptions.xmlNamespace().ifPresent(option -> { + try { + var uri = new java.net.URI(option.value()); + if (option.value().isEmpty() || option.value().equals("http://www.w3.org/2000/xmlns/") + || option.value().equals("http://www.w3.org/XML/1998/namespace")) + throw new java.net.URISyntaxException(option.value(), "reserved or empty namespace"); + } catch (java.net.URISyntaxException invalid) { + diagnostics.emitRule(decl, "XMLNS must be a nonempty, unreserved namespace URI reference", "MODEL_XMLNS", ((Origin.DeclaredOrigin)option.origin()).range()); + } + }); + guru.interlis.syntax.ModelSyntax syntax = entry.model().syntax(); + Optional translation = syntax.translationSource() + .map(source -> new guru.interlis.model.ModelTranslation(source.text(), + syntax.translationVersion(), syntax.language(), Optional.empty(), List.of())); + translation=overlays.translation(decl.id(),translation); + state.defineSymbol(new ModelSymbol(header(decl), syntax.modelKind(), + syntax.contracted(), entry.model().version(), syntax.issuer(), imports, members, + translation, syntax.options(), modelNamespace(syntax))); + } + + private String modelNamespace(guru.interlis.syntax.ModelSyntax syntax) { + return syntax.languageVersion() == guru.interlis.model.LanguageVersion.ILI23 ? "http://www.interlis.ch/INTERLIS2.3" + : syntax.options().xmlNamespace().map(guru.interlis.model.ModelOptions.TextValue::value) + .orElse("http://www.interlis.ch/xtf/2.4/" + syntax.name()); + } + + /** Translations share the original transfer vocabulary, independent of file/declaration order. */ + void bindTransferNamespaces() { + for (var symbol : List.copyOf(state.symbolsInBuildOrder())) { + if (!(symbol instanceof ModelSymbol model) || model.translation().isEmpty()) continue; + Set seen = new HashSet<>(); + ModelSymbol original = model; + while (original.translation().flatMap(guru.interlis.model.ModelTranslation::sourceTarget).isPresent() + && seen.add(original.id())) { + work.checkpoint(); + var next = state.symbol(original.translation().orElseThrow().sourceTarget().orElseThrow()); + if (!(next instanceof ModelSymbol base)) break; + original = base; + } + String namespace = original.transferNamespace(); + if (model.options().xmlNamespace().filter(value -> !value.value().equals(namespace)).isPresent()) + diagnostics.emitRule(index.byId(model.id()).orElseThrow(), "translated XMLNS conflicts with the original transfer namespace", "TRANSLATION_XMLNS"); + state.replaceSymbol(new ModelSymbol(model.header(), model.modelKind(), model.contracted(), model.modelVersion(), + model.issuer(), model.imports(), model.members(), model.translation(), model.options(), namespace)); + } + } + + Symbol buildSymbol(DeclInfo decl) { + work.checkpoint(); + SymbolId id = decl.id(); + Symbol existing = state.symbol(id); + if (existing != null) { + return existing; + } + boolean outermost = provisionalSymbols.putIfAbsent(id, Optional.empty()) == null; + try { + Symbol symbol = bindDeclaration(decl); + if (!id.equals(symbol.id())) + throw new IllegalStateException("Binder returned a different declaration: " + id); + Symbol published = state.symbol(id); + if (published != symbol) { + // Complete only the exact value published by a recursive dispatch of this declaration. + if (published != null && provisionalSymbols.get(id).orElse(null) == published) + state.replaceSymbol(symbol); + else + state.defineSymbol(symbol); + } + if (!outermost) provisionalSymbols.put(id, Optional.of(symbol)); + return symbol; + } finally { + if (outermost) provisionalSymbols.remove(id); + } + } + + private Symbol bindDeclaration(DeclInfo decl) { + return switch (decl.kind()) { + case TOPIC -> buildTopic(decl); + case CLASS -> buildClass(decl); + case STRUCTURE -> buildStructure(decl); + case ASSOCIATION -> buildAssociation(decl); + case DOMAIN -> buildDomain(decl); + case UNIT -> units.buildUnit(decl); + case LINE_FORM -> lineForms.buildForm(decl); + case METADATA_BASKET -> metadata.basket(decl); + case METAOBJECT -> metadata.object(decl); + case FUNCTION -> buildFunction(decl); + case PARAMETER -> buildParameter(decl); + case CONTEXT -> generics.buildContext(decl); + case ATTRIBUTE -> buildAttribute(decl); + case ROLE -> buildRole(decl); + case CONSTRAINT -> constraints.buildConstraint(decl); + case ENUM_ELEMENT -> buildEnumElement(decl); + case UNSUPPORTED -> buildUnsupported(decl); + case GRAPHIC -> graphics.build(decl); + case DRAWING_RULE -> { + buildSymbol(index.byId(decl.owner().orElseThrow()).orElseThrow()); + yield state.symbol(decl.id()); + } + case VIEW -> views.build(decl); + case VIEW_BASE -> views.buildBase(decl); + case MODEL -> throw new IllegalStateException("model symbols are built separately"); + }; + } + + private TopicSymbol buildTopic(DeclInfo decl) { + TopicSyntax syntax = (TopicSyntax) decl.syntax(); + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + buildSymbol(child); + } + if (syntax.endName().isPresent() && !syntax.endName().orElseThrow().equals(syntax.name())) { + diagnostics.emitRule(decl, "TOPIC end name does not match declaration", "TOPIC_END_NAME"); + } + Optional base = syntax.baseTopic().flatMap(name -> { + var resolution = resolver.resolve(decl.id(), name, Set.of(SymbolKind.TOPIC)); + references.deferBaseAmbiguity(decl, name, resolution); + if (resolution.status() == NameResolver.Status.AMBIGUOUS) + names.reportAmbiguity(name, resolution); + else if (resolution.status() == NameResolver.Status.UNRESOLVED) + diagnostics.unresolved(name); + return resolution.target(); + }); + base.ifPresent(id -> inheritance.bindBase(decl.id(), id)); + if (base.filter(inheritance::isFinalDeclaration).isPresent()) + diagnostics.emitRule(decl, "cannot extend FINAL topic", "TOPIC_FINAL"); + if (base.isPresent() && state.symbol(base.orElseThrow()) instanceof TopicSymbol baseTopic) { + boolean abstractBase = baseTopic.header().modifiers() + .contains(guru.interlis.model.Modifier.ABSTRACT); + for (SymbolId member : baseTopic.members()) { + work.checkpoint(); + if (state.symbol(member) instanceof ClassSymbol baseClass + && baseClass.header().modifiers() + .contains(guru.interlis.model.Modifier.ABSTRACT)) { + abstractBase = true; + } + } + Set baseDomains = new LinkedHashSet<>(); + for (SymbolId member : baseTopic.members()) { + work.checkpoint(); + if (state.symbol(member) instanceof DomainSymbol domain) { + baseDomains.add(domain.name().value()); + } + } + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + if (child.kind() == SymbolKind.DOMAIN && baseDomains.contains(child.name())) { + diagnostics.emitRule(child, "domain name conflicts with an inherited topic member", + "INHERITED_NAME_CONFLICT"); + } + } + boolean baseHasConcrete = base.flatMap(index::byId).map(baseDecl -> { + for (DeclInfo child : index.children(baseDecl.id())) { + work.checkpoint(); + if (child.kind() == SymbolKind.STRUCTURE) { + return true; + } + if (child.kind() == SymbolKind.CLASS + && !child.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) { + return true; + } + } + return false; + }).orElse(false); + boolean derivedHasContent = !index.children(decl.id()).isEmpty() + || (decl.syntax() instanceof guru.interlis.syntax.TopicSyntax topicSyntax + && !topicSyntax.oidRules().isEmpty()) + || !syntax.deferredGenerics().isEmpty(); + if (abstractBase && !derivedHasContent && !baseHasConcrete && !syntax.modifiers() + .contains(guru.interlis.model.Modifier.ABSTRACT)) { + diagnostics.emitRule(decl, "empty topic extending an abstract topic must be abstract", + "ABSTRACT_TOPIC_EXTENSION"); + } + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + if (child.kind() != SymbolKind.CLASS + || !(state.symbol(child.id()) instanceof ClassSymbol derivedClass) + || derivedClass.baseType().isEmpty()) { + continue; + } + for (SymbolId member : baseTopic.members()) { + work.checkpoint(); + if (!(state.symbol(member) instanceof ClassSymbol baseClass) + || !baseClass.name().equals(derivedClass.name())) { + continue; + } + if (!child.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + diagnostics.emitRule(child, "class in an extending topic may not extend the " + + "same-named base class without EXTENDED", "TOPIC_CLASS_EXTENSION"); + } + if (baseClass.header().modifiers() + .contains(guru.interlis.model.Modifier.EXTENDED)) { + diagnostics.emitRule(child, "cannot extend a class marked EXTENDED", + "EXTENDED_BASE_CLASS"); + } + } + } + } + List dependencies = new ArrayList<>(); + for (guru.interlis.syntax.NameSyntax dependency : syntax.dependencies()) { + work.checkpoint(); + var resolution = resolver.resolve(decl.id(), dependency, Set.of(SymbolKind.TOPIC)); + Optional resolved = resolution.target(); + if (resolved.isEmpty()) diagnostics.unresolved(dependency); + resolved.ifPresent(dependencies::add); + references.recordResolution(decl, dependency, resolution, ReferenceUsage.TYPE_USE); + } + Set memberNames = new LinkedHashSet<>(); + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + if (child.kind() == SymbolKind.CLASS || child.kind() == SymbolKind.DOMAIN) { + if (!memberNames.add(child.name())) { + diagnostics.emitRule(child, "name conflict in topic", "TOPIC_MEMBER_NAME"); + } + } + } + if (!syntax.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) { + List classes = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.CLASS).toList(); + for (DeclInfo child : classes) { + work.checkpoint(); + if (child.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT) + && classes.stream().noneMatch(candidate -> + !candidate.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT) + && inheritance.baseChain(candidate.id()).contains(child.id()))) + diagnostics.emitRule(decl, "abstract class requires an abstract topic or a concrete specialization", + "ABSTRACT_TOPIC"); + } + } + List members = index.children(decl.id()).stream().map(DeclInfo::id).toList(); + List deferred = new ArrayList<>(); + for (var name : syntax.deferredGenerics()) names.bindTarget(decl, name, Set.of(SymbolKind.DOMAIN), + ReferenceUsage.TYPE_USE, new ArrayList<>()).ifPresent(deferred::add); + return new TopicSymbol(header(decl), base, members, dependencies, deferred, List.of()); + } + + private ClassSymbol buildClass(DeclInfo decl) { + ClassDeclarationSyntax syntax = (ClassDeclarationSyntax) decl.syntax(); + if (syntax.endName().isPresent() && !syntax.endName().orElseThrow().equals(syntax.name())) { + diagnostics.emitRule(decl, "CLASS end name does not match declaration", "CLASS_END_NAME"); + } + Optional base = inheritance.objectInheritanceBase(decl.id()); + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + buildSymbol(child); + } + List members = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.ATTRIBUTE || child.kind() == SymbolKind.ROLE || child.kind() == SymbolKind.PARAMETER) + .map(DeclInfo::id).toList(); + List constraints = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.CONSTRAINT) + .map(DeclInfo::id).toList(); + for (SymbolId member : members) { + work.checkpoint(); + DeclInfo memberDecl = index.byId(member).orElse(null); + if (memberDecl != null && memberDecl.kind() == SymbolKind.ATTRIBUTE) { + TypeId typeId = state.declaredType(member); + if (decl.language() != guru.interlis.model.LanguageVersion.ILI24 + && typeId != null && state.type(typeId) instanceof ReferenceType) { + diagnostics.emitRule(memberDecl, "reference attribute is only allowed in a structure", + "REFERENCE_ATTRIBUTE_CLASS"); + } + } + } + return new ClassSymbol(header(decl), base, members, constraints); + } + + private StructureSymbol buildStructure(DeclInfo decl) { + StructureDeclarationSyntax syntax = (StructureDeclarationSyntax) decl.syntax(); + Optional base = inheritance.objectInheritanceBase(decl.id()); + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + buildSymbol(child); + } + List members = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.ATTRIBUTE || child.kind() == SymbolKind.ROLE || child.kind() == SymbolKind.PARAMETER) + .map(DeclInfo::id).toList(); + List constraints = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.CONSTRAINT) + .map(DeclInfo::id).toList(); + return new StructureSymbol(header(decl), base, members, constraints); + } + + private AssociationSymbol buildAssociation(DeclInfo decl) { + AssociationDeclarationSyntax syntax = (AssociationDeclarationSyntax) decl.syntax(); + buildingAssociations.add(decl.id()); + var explicitBase = syntax.baseAssociation() + .map(name -> resolver.resolve(decl.id(), name, Set.of(SymbolKind.ASSOCIATION))); + Optional base = explicitBase.flatMap(NameResolver.Resolution::target); + if (explicitBase.isPresent()) { + var name = syntax.baseAssociation().orElseThrow(); + references.deferBaseAmbiguity(decl, name, explicitBase.orElseThrow()); + if (base.isEmpty()) diagnostics.unresolved(name); + } + if (base.isEmpty() && explicitBase.stream().noneMatch(r -> r.status() == NameResolver.Status.AMBIGUOUS) + && syntax.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + base = inheritance.baseTopicMember(decl.id(), decl.name(), Set.of(SymbolKind.ASSOCIATION)); + if (base.isEmpty()) diagnostics.emitRule(decl, "EXTENDED association needs an inherited base", "ASSOCIATION_BASE"); + } + if (base.isPresent()) { + SymbolId parent = base.orElseThrow(); + if (buildingAssociations.contains(parent)) { + diagnostics.emitRule(decl, "cyclic association inheritance", "INHERITANCE_CYCLE"); + base = Optional.empty(); + } else { + buildSymbol(index.byId(parent).orElseThrow()); + inheritance.bindBase(decl.id(), parent); + if (state.symbol(parent).header().modifiers().contains(guru.interlis.model.Modifier.FINAL)) { + diagnostics.emitRule(decl, "cannot extend FINAL association", "ASSOCIATION_FINAL"); + } + } + } + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + buildSymbol(child); + } + buildingAssociations.remove(decl.id()); + if (syntax.nameRange().isPresent() != syntax.endName().isPresent()) { + diagnostics.emitRule(decl, "association name must be present at both begin and END", "ASSOCIATION_NAME"); + } + Set roleNames = new LinkedHashSet<>(); + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + if (child.kind() == SymbolKind.ROLE && !roleNames.add(child.name())) { + diagnostics.emitRule(child, "duplicate role name", "DUPLICATE_ROLE"); + } + } + List roles = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.ROLE) + .map(DeclInfo::id).toList(); + List attributes = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.ATTRIBUTE) + .map(DeclInfo::id).toList(); + if (roles.isEmpty() && attributes.isEmpty() && base.isEmpty()) { + diagnostics.emitRule(decl, "association requires at least two roles", "ASSOCIATION_ROLES"); + } + List constraints = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.CONSTRAINT) + .map(DeclInfo::id).toList(); + Cardinality inheritedCardinality = base.map(state::symbol).map(AssociationSymbol.class::cast) + .map(AssociationSymbol::cardinality).orElse(Cardinality.exactly(1)); + Cardinality effectiveCardinality = syntax.cardinality().orElse(inheritedCardinality); + if (base.isPresent() && (effectiveCardinality.lower().compareTo(inheritedCardinality.lower()) < 0 + || widerUpper(effectiveCardinality.upper(), inheritedCardinality.upper()))) { + diagnostics.emitRule(decl, "association extension widens link cardinality", "ASSOCIATION_CARDINALITY_EXTENSION"); + } + return new AssociationSymbol(header(decl), base, effectiveCardinality, roles, attributes, constraints, + index.children(decl.id()).stream().filter(d -> d.kind() == SymbolKind.VIEW_BASE).map(DeclInfo::id).findFirst()); + } + + private guru.interlis.model.ParameterSymbol buildParameter(DeclInfo decl) { + var syntax = (guru.interlis.syntax.ParameterDeclarationSyntax) decl.syntax(); + var model = index.modelOfSymbol(decl.id()).flatMap(index::byId) + .map(m -> (guru.interlis.syntax.ModelSyntax) m.syntax()).orElseThrow(); + if (index.byId(decl.owner().orElseThrow()).orElseThrow().kind() == SymbolKind.MODEL + && decl.language() == guru.interlis.model.LanguageVersion.ILI23 && !model.contracted()) + diagnostics.emitRule(decl, "runtime parameters require CONTRACTED in INTERLIS 2.3", "PARAMETER_CONTRACTED"); + TypeId type = typeBindings.buildType(syntax.type(), decl.id(), decl.sourceId()); + if (index.byId(decl.owner().orElseThrow()).orElseThrow().kind() == SymbolKind.MODEL && containsArea(type, new HashSet<>())) + diagnostics.emitRule(decl, "runtime parameters cannot contain AREA values", "PARAMETER_AREA"); + typeBindings.bindDeclaredType(decl.id(), type); + return new guru.interlis.model.ParameterSymbol(header(decl), type, syntax.mandatory() || typeBindings.mandatoryType(type), Optional.empty()); + } + + private boolean containsArea(TypeId type, Set seen) { + if (!seen.add(type)) return false; + TypeDescriptor descriptor = typeBindings.effectiveDescriptor(type); + if (descriptor instanceof GeometryType geometry) + return geometry.geometryKind() == guru.interlis.model.GeometryKind.AREA + || geometry.geometryKind() == guru.interlis.model.GeometryKind.MULTIAREA; + if (descriptor instanceof CollectionType collection) return containsArea(collection.elementType(), seen); + if (descriptor instanceof StructureValueType structure) { + for(SymbolId root:structure.targetRoots()) { + Set visited=new HashSet<>(); + for(SymbolId current=root;current!=null && visited.add(current);current=inheritance.objectInheritanceBase(current).orElse(null)) { + work.checkpoint(); + if(state.symbol(current) instanceof StructureSymbol known) { + for(SymbolId member:known.members()) + if(state.symbol(member) instanceof AttributeSymbol attribute && containsArea(attribute.declaredType(),seen))return true; + } else for(var child:index.children(current)) + if(child.kind()==SymbolKind.ATTRIBUTE && containsArea(typeBindings.attributeType(child),seen))return true; + } + } + } + return false; + } + + private FunctionSymbol buildFunction(DeclInfo decl) { + FunctionDeclarationSyntax syntax = (FunctionDeclarationSyntax) decl.syntax(); + var model = index.modelOfSymbol(decl.id()).flatMap(index::byId) + .map(m -> (guru.interlis.syntax.ModelSyntax) m.syntax()); + if (decl.language() == guru.interlis.model.LanguageVersion.ILI23 + && model.isPresent() && !model.orElseThrow().contracted()) { + diagnostics.emitRule(decl, "functions require CONTRACTED in INTERLIS 2.3", "FUNCTION_CONTRACTED"); + } + Set names = new LinkedHashSet<>(); + for (var parameter : syntax.parameters()) { + work.checkpoint(); + if (!names.add(parameter.name())) { + diagnostics.emitRule(decl, "duplicate function parameter", "FUNCTION_PARAMETER_DUPLICATE"); + } + } + List parameters = java.util.stream.IntStream.range(0, syntax.parameters().size()) + .mapToObj(i -> new FunctionParameter(syntax.parameters().get(i).name(), typeBindings.functionArgumentType(decl, i), syntax.parameters().get(i).mandatory() || typeBindings.mandatoryType(typeBindings.functionArgumentType(decl,i)))).toList(); + TypeId result=typeBindings.buildType(syntax.returnType(), decl.id(), decl.sourceId()); + return new FunctionSymbol(header(decl), parameters,result,syntax.returnMandatory() || typeBindings.mandatoryType(result),syntax.explanation().map(e->new Documentation(e.text(),List.of(e.range()))).orElseGet(Documentation::empty)); + } + + private DomainSymbol buildDomain(DeclInfo decl) { + TypeId type = typeBindings.domainType(decl); + Set constraintNames = new LinkedHashSet<>(); + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + if (child.kind() == SymbolKind.CONSTRAINT && !constraintNames.add(child.name())) { + diagnostics.emitRule(child, "domain constraint names must be unique", "DOMAIN_CONSTRAINT_NAME"); + } + buildSymbol(child); + } + return new DomainSymbol(header(decl), type, Optional.ofNullable(state.baseOf(decl.id())), + decl.modifiers().contains(guru.interlis.model.Modifier.GENERIC)); + } + + private AttributeSymbol buildAttribute(DeclInfo decl) { + AttributeDeclarationSyntax syntax = (AttributeDeclarationSyntax) decl.syntax(); + TypeId type = typeBindings.attributeType(decl); + for(var child:index.children(decl.id()))if(child.kind()==SymbolKind.ENUM_ELEMENT)buildSymbol(child); + if (syntax.transientAttribute() && !syntax.assigned() + && !syntax.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) { + diagnostics.emitRule(decl, "transient attribute requires an assignment", "TRANSIENT_ASSIGNMENT"); + } + return new AttributeSymbol(header(decl), type, syntax.mandatory() || typeBindings.mandatoryType(type), + syntax.transientAttribute(), syntax.continuous(), syntax.subdivision(), + Optional.empty(), bindAttributeDerivations(decl, syntax, type)); + } + + private List bindAttributeDerivations(DeclInfo decl, AttributeDeclarationSyntax syntax, TypeId type) { + List inferred = typeBindings.inferredDerivations(decl.id()); + if (inferred != null) return inferred; + List result = new ArrayList<>(); + for (var expression : syntax.derivations()) { + var bound = expressions.contextualValue(expressions.buildExpression(expression, decl.owner().orElseThrow()),type,decl.id()); + result.add(bound.id()); + if(!bound.error() && (syntax.mandatory() || typeBindings.mandatoryType(type)) + && guru.interlis.model.internal.TypeCompatibility.isUndefined(typeBindings.effectiveDescriptor(bound.typeId()))) + diagnostics.emitRule(decl,"MANDATORY attribute excludes an explicitly undefined derived value","MANDATORY_VALUE",expression.range()); + if (!bound.error() && !typeBindings.assignable(bound.typeId(), type)) + diagnostics.emitTypeMismatch(decl.sourceId(), expression.range(), typeBindings.typeName(bound.typeId()), typeBindings.typeName(type)); + } + return List.copyOf(result); + } + + private RoleSymbol buildRole(DeclInfo decl) { + RoleDeclarationSyntax syntax = (RoleDeclarationSyntax) decl.syntax(); + List targets = new ArrayList<>(); + for (var target : syntax.targets()) { + work.checkpoint(); + SymbolId base = bindRoleTarget(decl, target.base()); + List restrictions = target.restrictions().stream() + .map(name -> bindRoleTarget(decl, name)).toList(); + for (SymbolId restriction : restrictions) { + if (!inheritance.targetSpecializes(restriction, base)) { + diagnostics.emitRule(decl, "restriction must specialize its role target", "ROLE_TARGET_RESTRICTION"); + } + } + for (var previous : targets) { + if (inheritance.targetSpecializes(base, previous.base()) || inheritance.targetSpecializes(previous.base(), base)) { + diagnostics.emitRule(decl, "role alternatives must not specialize each other", "ROLE_TARGET_OVERLAP"); + } + } + targets.add(new guru.interlis.model.RoleTarget(base, restrictions)); + } + Cardinality cardinality = syntax.cardinality(); + SymbolId associationBase = state.baseOf(decl.owner().orElseThrow()); + RoleSymbol inherited = associationBase == null ? null : inheritance.associationRoles(associationBase).stream() + .filter(r -> r.name().value().equals(decl.name())).findFirst().orElse(null); + if (inherited == null) { + if (associationBase != null) diagnostics.emitRule(decl, "extended association must not add roles", "ASSOCIATION_EXTENDED_ROLE"); + if (decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED) + || decl.modifiers().contains(guru.interlis.model.Modifier.HIDING)) { + diagnostics.emitRule(decl, "role extension requires an inherited role", "ROLE_EXTENSION_BASE"); + } + } else { + if (!decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + diagnostics.emitRule(decl, "role override requires EXTENDED", "ROLE_EXTENSION"); + } + if (inherited.header().modifiers().contains(guru.interlis.model.Modifier.FINAL)) { + diagnostics.emitRule(decl, "cannot override FINAL role", "ROLE_FINAL"); + } + if (!syntax.explicitCardinality()) cardinality = inherited.cardinality(); + if (cardinality.lower().compareTo(inherited.cardinality().lower()) < 0 + || widerUpper(cardinality.upper(), inherited.cardinality().upper()) + || syntax.aggregation().ordinal() < inherited.aggregation().ordinal()) { + diagnostics.emitRule(decl, "extended role widens cardinality or weakens aggregation", "ROLE_EXTENSION"); + } + if (inherited.external() != decl.modifiers().contains(guru.interlis.model.Modifier.EXTERNAL)) { + diagnostics.emitRule(decl, "EXTERNAL must remain unchanged in an extended role", "ROLE_EXTERNAL_EXTENSION"); + } + if (inherited.ordered() && !decl.modifiers().contains(guru.interlis.model.Modifier.ORDERED)) { + diagnostics.emitRule(decl, "an ordered role cannot become unordered in an extension", "ROLE_ORDER_EXTENSION"); + } + if (targets.stream().flatMap(t -> t.allowedRoots().stream()).anyMatch(t -> !inheritance.roleAccepts(inherited, t))) { + diagnostics.emitRule(decl, "extended role widens its target set", "ROLE_TARGET_EXTENSION"); + } + } + return new RoleSymbol(header(decl), targets, cardinality, syntax.aggregation(), + decl.modifiers().contains(guru.interlis.model.Modifier.ORDERED), + decl.modifiers().contains(guru.interlis.model.Modifier.EXTERNAL), + decl.modifiers().contains(guru.interlis.model.Modifier.HIDING), bindRoleDerivation(decl, syntax, targets)); + } + + private Optional bindRoleDerivation(DeclInfo decl, RoleDeclarationSyntax syntax, List targets) { + return syntax.derivation().map(expression -> { + var bound = expressions.buildExpression(expression, decl.owner().orElseThrow()); + var actual = typeBindings.effectiveDescriptor(bound.typeId()); + if (!bound.error()) { + var roots = targets.stream().flatMap(t -> t.allowedRoots().stream()).toList(); + var expected = new guru.interlis.model.ObjectArgumentType(typeBindings.nextTypeId(), decl.origin(), Optional.empty(), roots, false); + typeBindings.defineType(expected); + if (!typeBindings.assignable(bound.typeId(), expected.id())) diagnostics.emitTypeMismatch(decl.sourceId(), expression.range(), typeBindings.typeName(bound.typeId()), "single role target"); + } + return bound.id(); + }); + } + + SymbolId bindObjectTarget(DeclInfo decl, guru.interlis.syntax.NameSyntax name, ReferenceUsage usage) { + if (name.text().equals("ANYCLASS")) { + SymbolId target = anyClass(decl); + references.recordReference(decl, name, target, usage); + return target; + } + var kinds = Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION); + var resolution = resolver.resolve(decl.id(), name, kinds); + if (resolution.status() == NameResolver.Status.UNRESOLVED && name.segments().size() == 1) { + var inherited = inheritance.baseTopicMember(decl.id(), name.text(), kinds); + if (inherited.isPresent()) { + references.recordReference(decl, name, inherited.orElseThrow(), usage); + return inherited.orElseThrow(); + } + } + if (resolution.target().isEmpty()) diagnostics.unresolved(name); + references.recordResolution(decl, name, resolution, usage); + return resolution.target().orElseGet(() -> syntheticClass(decl, name)); + } + + private SymbolId bindRoleTarget(DeclInfo decl, guru.interlis.syntax.NameSyntax name) { + var target=bindObjectTarget(decl, name, ReferenceUsage.ROLE_TARGET); + return target; + } + + private SymbolId anyClass(DeclInfo owner) { + SymbolId model = index.modelOfSymbol(owner.id()).orElseThrow(); + SymbolId id = new SymbolId(model.value() + ":predefined:ANYCLASS"); + if (!state.hasSymbol(id)) { + state.defineSymbol(new ClassSymbol(new SymbolHeader(id, SymbolKind.CLASS, + DeclaredName.declared("ANYCLASS"), Optional.of(model), 0, + new Origin.PredefinedOrigin(owner.language(), "INTERLIS.ANYCLASS"), Optional.empty(), + owner.language(), Documentation.empty(), MetaAttributeSet.empty(), Set.of(), List.of()), + Optional.empty(), List.of(), List.of())); + } + return id; + } + + private SymbolId syntheticClass(DeclInfo owner, guru.interlis.syntax.NameSyntax name) { + SymbolId id = new SymbolId("sym-unresolved-" + typeBindings.nextTypeSequence()); + SymbolId model = index.modelOfSymbol(owner.id()).orElse(owner.id()); + SymbolHeader header = new SymbolHeader(id, SymbolKind.CLASS, + DeclaredName.synthetic(""), + Optional.of(model), 0, + new Origin.SyntheticOrigin(guru.interlis.model.SyntheticReason.MISSING_REFERENCE, + Optional.of(name.range())), + Optional.empty(), owner.language(), Documentation.empty(), + MetaAttributeSet.empty(), Set.of(), List.of()); + ClassSymbol synthetic = new ClassSymbol(header, Optional.empty(), + List.of(), List.of()); + state.defineSymbol(synthetic); + return id; + } + + private boolean widerUpper(guru.interlis.model.CardinalityUpper left, + guru.interlis.model.CardinalityUpper right) { + if (left instanceof guru.interlis.model.CardinalityUpper.Unbounded) { + return !(right instanceof guru.interlis.model.CardinalityUpper.Unbounded); + } + if (right instanceof guru.interlis.model.CardinalityUpper.Unbounded) { + return false; + } + if (left instanceof guru.interlis.model.CardinalityUpper.Finite leftFinite + && right instanceof guru.interlis.model.CardinalityUpper.Finite rightFinite) { + return leftFinite.value().compareTo(rightFinite.value()) > 0; + } + return false; + } + + private EnumElementSymbol buildEnumElement(DeclInfo decl) { + String path = decl.name(); + List segments = List.of(path.split("\\.")); + TypeDescriptor type = decl.owner().map(state::declaredType).map(typeBindings::effectiveDescriptor).orElse(null); + int ordinal = type instanceof EnumerationType enumeration ? enumeration.values().stream() + .filter(value -> value.path().equals(path)).mapToInt(EnumerationValue::order).findFirst().orElse(-1) : -1; + if (ordinal < 0) { + // Erroneous partial types still retain local declaration order, never + // the unrelated global declaration counter. + ordinal = index.children(decl.owner().orElseThrow()).stream().filter(d -> d.kind() == SymbolKind.ENUM_ELEMENT) + .map(DeclInfo::id).toList().indexOf(decl.id()); + } + return new EnumElementSymbol(header(decl), segments, ordinal); + } + + private UnsupportedSymbol buildUnsupported(DeclInfo decl) { + String feature = decl.featureId().orElse("UNKNOWN_SYNTAX"); + diagnostics.emitUnsupported(decl.sourceId(), decl.origin() instanceof Origin.DeclaredOrigin d + ? d.range() : null, feature, decl.name()); + return new UnsupportedSymbol(header(decl), feature, decl.name()); + } + + SymbolHeader header(DeclInfo decl) { + var values=new LinkedHashMap(); + var entries=new ArrayList(); + for(var attribute:decl.metadata().attributes()) { + var value=new MetaAttributeSet.Value(attribute.text(),new Origin.DeclaredOrigin(attribute.range())); + entries.add(new MetaAttributeSet.Entry(attribute.key(),value)); + var previous=values.putIfAbsent(attribute.key(),value); + boolean repeatable=attribute.key().equals("fn.param") && decl.syntax() instanceof FunctionDeclarationSyntax; + if(previous!=null && !previous.text().equals(attribute.text()) && !repeatable)diagnostics.emitRule(decl,"conflicting source metaattribute: "+attribute.key(),"METAATTRIBUTE_CONFLICT",attribute.range()); + } + var configured=overlays.metadata(decl.id()); + if(!configured.isEmpty()) { + var combined=new ArrayList(configured);combined.addAll(entries);entries=combined;values.clear(); + for(var entry:entries)values.putIfAbsent(entry.key(),entry.value()); + } + var documentation=new Documentation(decl.metadata().documentation().stream().map(guru.interlis.syntax.SourceMetadata.Text::text).collect(java.util.stream.Collectors.joining("\n\n")), + decl.metadata().documentation().stream().map(guru.interlis.syntax.SourceMetadata.Text::range).toList()); + DeclaredName declaredName = isSyntheticName(decl.name()) + ? DeclaredName.synthetic(decl.name()) : DeclaredName.declared(decl.name()); + Set modifiers = new LinkedHashSet<>(decl.modifiers()); + if (decl.syntax() instanceof AttributeDeclarationSyntax attribute + && attribute.transientAttribute()) { + modifiers.add(guru.interlis.model.Modifier.TRANSIENT); + } + if (decl.kind() == SymbolKind.DOMAIN && typeBindings.mandatoryDomain(decl.id())) modifiers.add(guru.interlis.model.Modifier.MANDATORY); + return new SymbolHeader(decl.id(), decl.kind(), declaredName, decl.owner(), + decl.order(), decl.origin(), decl.nameRange(), decl.language(), + documentation, new MetaAttributeSet(values,entries), modifiers, List.of()); + } + + private static boolean isSyntheticName(String name) { + return name.startsWith("<") || name.startsWith("constraint@") + || name.startsWith("unsupported@") || name.startsWith("Association@"); + } + + Optional bindObjectTarget(DeclInfo owner, + guru.interlis.syntax.NameSyntax name, List errors) { + return names.bindTarget(owner, name, Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION, SymbolKind.VIEW), ReferenceUsage.TYPE_USE, errors); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ExactUnitFactors.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ExactUnitFactors.java index f1b5668b..96118192 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ExactUnitFactors.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ExactUnitFactors.java @@ -8,16 +8,22 @@ /** Exact factor comparisons, without evaluating FUNCTION explanations or rounding named constants. */ final class ExactUnitFactors { + private final SemanticGraphRead state; + private final SemanticWork work; enum Result { MATCH, MISMATCH, UNKNOWN } - private final GraphBuilder graph; private final Map ranks=new HashMap<>(); - ExactUnitFactors(GraphBuilder graph) {this.graph=graph;} + + ExactUnitFactors(SemanticGraphRead state, + SemanticWork work) { + this.state = state; + this.work = work; + } Result compare(SymbolId coarse,SymbolId fine,BigDecimal period) { var relation=TypeCompatibility.unitRelation(coarse,fine,this::unit); if(relation.isEmpty())return Result.UNKNOWN; // Existing unit diagnostics own invalid dependencies. if(relation.orElseThrow().decision()==RelationDecision.DISALLOWED) - return TypeCompatibility.definitelyDifferentUnitDimensions(coarse,fine,this::unit,graph::checkWork)?Result.MISMATCH:Result.UNKNOWN; + return TypeCompatibility.definitelyDifferentUnitDimensions(coarse,fine,this::unit,work::checkpoint)?Result.MISMATCH:Result.UNKNOWN; if(coarse.equals(fine))return period.compareTo(BigDecimal.ONE)==0?Result.MATCH:Result.MISMATCH; if(!rank(coarse)||!rank(fine))return Result.UNKNOWN; var queue=new PriorityQueue(Comparator.comparingInt(ranks::get).reversed().thenComparing(SymbolId::value)); @@ -27,14 +33,14 @@ Result compare(SymbolId coarse,SymbolId fine,BigDecimal period) { var constants=new EnumMap(UnitSymbol.ConstantKind.class); enqueue(terms,queue,coarse,BigInteger.ONE);enqueue(terms,queue,fine,BigInteger.ONE.negate()); while(!queue.isEmpty()) { - graph.checkWork();var id=queue.remove();var power=terms.remove(id);if(power==null)continue; + work.checkpoint();var id=queue.remove();var power=terms.remove(id);if(power==null)continue; var unit=unit(id);if(unit==null)return Result.UNKNOWN; if(unit.conversionFunction().isPresent()||unit.derivedFrom().isEmpty()&&unit.components().isEmpty()) { add(roots,id,power);continue; } if(unit.derivedFrom().isPresent()) { for(var factor:unit.conversionFactors()) { - graph.checkWork();var signed=factor.operator()==UnitSymbol.Operator.MULTIPLY?power:power.negate(); + work.checkpoint();var signed=factor.operator()==UnitSymbol.Operator.MULTIPLY?power:power.negate(); if(factor.kind()==UnitSymbol.ConstantKind.DECIMAL) { var number=factor.decimal().orElseThrow().exactValue(); if(number.signum()==0)return Result.UNKNOWN; @@ -43,7 +49,7 @@ Result compare(SymbolId coarse,SymbolId fine,BigDecimal period) { } enqueue(terms,queue,unit.derivedFrom().orElseThrow(),power); } else for(var component:unit.components()) { - graph.checkWork();enqueue(terms,queue,component.unit(),component.operator()==UnitSymbol.Operator.MULTIPLY?power:power.negate()); + work.checkpoint();enqueue(terms,queue,component.unit(),component.operator()==UnitSymbol.Operator.MULTIPLY?power:power.negate()); } } // A common opaque base cancels before its definition is inspected. @@ -58,12 +64,12 @@ Result compare(SymbolId coarse,SymbolId fine,BigDecimal period) { add(numbers,period,BigInteger.ONE.negate()); Fraction result=new Fraction(BigInteger.ONE,BigInteger.ONE); for(var number:numbers.entrySet()) { - graph.checkWork();result=result.multiply(decimal(number.getKey()).power(number.getValue())); + work.checkpoint();result=result.multiply(decimal(number.getKey()).power(number.getValue())); } return result.numerator.equals(result.denominator)?Result.MATCH:Result.MISMATCH; } - private UnitSymbol unit(SymbolId id) {graph.checkWork();return graph.symbols.get(id) instanceof UnitSymbol unit?unit:null;} + private UnitSymbol unit(SymbolId id) {work.checkpoint();return state.symbol(id) instanceof UnitSymbol unit?unit:null;} private static List dependencies(UnitSymbol unit) { if(unit.conversionFunction().isPresent())return List.of(); if(unit.derivedFrom().isPresent())return List.of(unit.derivedFrom().orElseThrow()); @@ -73,14 +79,14 @@ private boolean rank(SymbolId root) { record Visit(SymbolId id,boolean exit) { } var pending=new ArrayDeque();var active=new HashSet();pending.push(new Visit(root,false)); while(!pending.isEmpty()) { - graph.checkWork();var visit=pending.pop();if(ranks.containsKey(visit.id()))continue; + work.checkpoint();var visit=pending.pop();if(ranks.containsKey(visit.id()))continue; var unit=unit(visit.id());if(unit==null)return false;var dependencies=dependencies(unit); if(!visit.exit()) { if(!active.add(visit.id()))return false; pending.push(new Visit(visit.id(),true)); for(var dependency:dependencies)if(!ranks.containsKey(dependency))pending.push(new Visit(dependency,false)); } else { - int rank=0;for(var dependency:dependencies) {graph.checkWork();rank=Math.max(rank,ranks.get(dependency)+1);} + int rank=0;for(var dependency:dependencies) {work.checkpoint();rank=Math.max(rank,ranks.get(dependency)+1);} ranks.put(visit.id(),rank);active.remove(visit.id()); } } @@ -94,24 +100,24 @@ private static void enqueue(Map terms,PriorityQueue=0?new Fraction(value.unscaledValue(),ten):new Fraction(product(value.unscaledValue(),ten),BigInteger.ONE); } private BigInteger gcd(BigInteger left,BigInteger right) { - graph.checkWork();if(left.abs().equals(right.abs()))return left.abs(); + work.checkpoint();if(left.abs().equals(right.abs()))return left.abs(); if(left.abs().equals(BigInteger.ONE)||right.abs().equals(BigInteger.ONE))return BigInteger.ONE; charge(left.bitLength(),right.bitLength());return left.gcd(right); } private BigInteger product(BigInteger left,BigInteger right) { - graph.checkWork();charge(left.bitLength(),right.bitLength());return left.multiply(right); + work.checkpoint();charge(left.bitLength(),right.bitLength());return left.multiply(right); } private void charge(long leftBits,long rightBits) { long work=1+leftBits*rightBits/4096; - for(long i=0;i views; + private final TypeBindings typeBindings; + private final LineForms lineForms; + private final MetadataBindings metadata; + private final NameBindings names; + private final ReferenceBindings references; + private final FunctionBindings functions; + private final UnitBindings units; + private final FormattedTypeBindings formats; + private final Consumer definePredefinedFunction; + private final BiFunction externalConstraintTarget; + private final Function graphicContext; + private final Function ensureSymbol; + + ExpressionBindings(SemanticGraphRead.Expressions state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + SourceSet sourceSet, + ObjectPathBindings paths, + Supplier views, + TypeBindings typeBindings, + LineForms lineForms, + MetadataBindings metadata, + NameBindings names, + ReferenceBindings references, + FunctionBindings functions, + UnitBindings units, + FormattedTypeBindings formats, + Consumer definePredefinedFunction, + BiFunction externalConstraintTarget, + Function graphicContext, + Function ensureSymbol) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.sourceSet = sourceSet; + this.paths = paths; + this.views = views; + this.typeBindings = typeBindings; + this.lineForms = lineForms; + this.metadata = metadata; + this.names = names; + this.references = references; + this.functions = functions; + this.units = units; + this.formats = formats; + this.definePredefinedFunction = definePredefinedFunction; + this.externalConstraintTarget = externalConstraintTarget; + this.graphicContext = graphicContext; + this.ensureSymbol = ensureSymbol; + } + + ExpressionResult buildExpression(ExpressionSyntax syntax, SymbolId owner) { + ExpressionId id = state.nextExpressionId(); + return switch (syntax) { + case LiteralExpressionSyntax literal -> literalExpression(id, literal, owner); + case PathExpressionSyntax path -> paths.pathExpression(id, path, owner); + case BinaryExpressionSyntax binary -> binaryExpression(id, binary, owner); + case UnaryExpressionSyntax unary -> unaryExpression(id, unary, owner); + case DefinedExpressionSyntax defined -> definedExpression(id, defined, owner); + case ConstantExpressionSyntax constant -> constantExpression(id, constant, owner); + case UndefinedExpressionSyntax undefined -> undefinedExpression(id, undefined, owner); + case ParenthesizedExpressionSyntax parenthesized -> + buildExpression(parenthesized.inner(), owner); + case FunctionCallExpressionSyntax call -> functionCallExpression(id, call, owner); + case BuiltinFunctionExpressionSyntax builtin -> builtinFunctionExpression(id, builtin, owner); + case EnumerationConstantExpressionSyntax constant -> enumerationConstantExpression(id, constant, owner); + case ClassConstantExpressionSyntax constant -> classConstantExpression(id, constant, owner); + case guru.interlis.syntax.ObjectSetExpressionSyntax set -> objectSetExpression(id, set, owner); + case guru.interlis.syntax.InspectionExpressionSyntax inspection -> views.get().factor(id, inspection, owner); + case GenericConstantExpressionSyntax constant -> genericConstantExpression(id, constant, owner); + case guru.interlis.syntax.ParameterExpressionSyntax parameter -> parameterExpression(id, parameter, owner); + case guru.interlis.syntax.AttributeConstantExpressionSyntax constant -> attributeConstantExpression(id, constant, owner); + case UnsupportedExpressionSyntax unsupported -> { + DiagnosticId diagnostic = diagnostics.emitUnsupported(ownerSource(owner), unsupported.range(), + unsupported.featureId(), unsupported.construct()); + TypeId error = typeBindings.errorType(ownerSource(owner), unsupported.range(), + unsupported.featureId()); + ErrorExpression expression = new ErrorExpression(id, error, + guru.interlis.model.ExpressionTypeState.ERROR, unsupported.range(), + List.of(), List.of(diagnostic)); + state.defineExpression(expression); + yield new ExpressionResult(id, error, true); + } + }; + } + + private ExpressionResult objectSetExpression(ExpressionId id, + guru.interlis.syntax.ObjectSetExpressionSyntax syntax, SymbolId owner) { + TypeId type; + if (syntax.selection().isPresent()) { + type = typeBindings.buildType(syntax.selection().orElseThrow(), owner, ownerSource(owner)); + } else { + SymbolId context = expressionContext(owner); + var kind = index.byId(context).map(DeclInfo::kind).orElse(null); + type = typeBindings.nextTypeId(); + if (kind == SymbolKind.CLASS || kind == SymbolKind.ASSOCIATION || kind == SymbolKind.VIEW) { + typeBindings.defineType(new guru.interlis.model.ObjectArgumentType(type, + new Origin.DeclaredOrigin(syntax.range()), Optional.of(context), List.of(), true)); + } else { + type = typeBindings.errorType(ownerSource(owner), syntax.range(), "ALL requires an object context"); + } + } + boolean error = typeBindings.effectiveDescriptor(type) instanceof ErrorType; + state.defineExpression(new guru.interlis.model.ObjectSetExpression(id, type, + error ? guru.interlis.model.ExpressionTypeState.ERROR : guru.interlis.model.ExpressionTypeState.RESOLVED, + syntax.range())); + return new ExpressionResult(id, type, error); + } + + SymbolId expressionContext(SymbolId owner) { + SymbolId context = owner; + while (index.byId(context).map(DeclInfo::kind).orElse(null) == SymbolKind.CONSTRAINT) { + work.checkpoint(); + context = externalConstraintTarget.apply(context, + index.byId(context).orElseThrow().owner().orElseThrow()); + } + return graphicContext.apply(context); + } + + private ExpressionResult genericConstantExpression(ExpressionId id, + GenericConstantExpressionSyntax syntax, + SymbolId owner) { + TypeId type = typeBindings.nextTypeId(); + typeBindings.defineType(new GenericType(type, new Origin.DeclaredOrigin(syntax.range()), syntax.name())); + state.defineExpression(new guru.interlis.model.ConstantExpression(id, type, + guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), + syntax.name())); + return new ExpressionResult(id, type, false); + } + + private ExpressionResult classConstantExpression(ExpressionId id, + ClassConstantExpressionSyntax syntax, + SymbolId owner) { + List errors = new ArrayList<>(); + var declaration = index.byId(owner).orElseThrow(); + var kinds = Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE, SymbolKind.ASSOCIATION, SymbolKind.VIEW); + var target = lineForms.predefined(declaration, syntax.target().text(), kinds); + if (target.isEmpty()) target = metadata.predefined(declaration, syntax.target().text(), kinds); + if (target.isEmpty()) target = names.bindTarget(declaration, syntax.target(), kinds, ReferenceUsage.EXPRESSION, errors); + else references.recordReference(declaration, syntax.target(), target.orElseThrow(), ReferenceUsage.EXPRESSION); + if (target.isEmpty()) return failedValueExpression(id, syntax.range(), errors); + TypeId type = typeBindings.nextTypeId(); + boolean structure = state.symbol(target.orElseThrow()) instanceof StructureSymbol + || index.byId(target.orElseThrow()).map(d -> d.kind() == SymbolKind.STRUCTURE).orElse(false); + typeBindings.defineType(new ClassValueType(type, new Origin.DeclaredOrigin(syntax.range()), structure, target.stream().toList())); + state.defineExpression(new guru.interlis.model.SymbolValueExpression(id, type, + guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), target.orElseThrow())); + return new ExpressionResult(id, type, false); + } + + private ExpressionResult attributeConstantExpression(ExpressionId id, guru.interlis.syntax.AttributeConstantExpressionSyntax syntax, SymbolId owner) { + return attributeConstantExpression(id,syntax,owner,expressionContext(owner)); + } + + ExpressionResult contextualAttributeConstant(guru.interlis.syntax.AttributeConstantExpressionSyntax syntax,SymbolId owner,SymbolId context) { + return attributeConstantExpression(state.nextExpressionId(),syntax,owner,context); + } + + private ExpressionResult attributeConstantExpression(ExpressionId id, guru.interlis.syntax.AttributeConstantExpressionSyntax syntax, SymbolId owner,SymbolId context) { + var declaration = index.byId(owner).orElseThrow(); + List errors = new ArrayList<>(); + Optional viewable = syntax.viewable().isPresent() + ? names.bindTarget(declaration, syntax.viewable().orElseThrow(), Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE, SymbolKind.ASSOCIATION, SymbolKind.VIEW), ReferenceUsage.EXPRESSION, errors) + : Optional.of(context); + Optional attribute = viewable.flatMap(target -> paths.findMember(target, syntax.attribute().text())) + .filter(target -> index.byId(target).map(DeclInfo::kind).orElse(null) == SymbolKind.ATTRIBUTE || state.symbol(target) instanceof AttributeSymbol); + references.recordReference(declaration, syntax.attribute(), attribute.orElse(null), ReferenceUsage.EXPRESSION); + if (attribute.isEmpty()) { + errors.add(diagnostics.unresolved(syntax.attribute())); + return failedValueExpression(id, syntax.range(), errors); + } + TypeId attributeType = index.byId(attribute.orElseThrow()).map(typeBindings::attributeType) + .orElseGet(() -> ((AttributeSymbol) state.symbol(attribute.orElseThrow())).declaredType()); + TypeId type = typeBindings.nextTypeId(); + typeBindings.defineType(new guru.interlis.model.AttributePathType(type, new Origin.DeclaredOrigin(syntax.range()), + Optional.of(new guru.interlis.model.AttributePathType.Viewable(viewable.orElseThrow())), List.of(attributeType))); + state.defineExpression(new guru.interlis.model.SymbolValueExpression(id, type, guru.interlis.model.ExpressionTypeState.RESOLVED, + syntax.range(), attribute.orElseThrow())); + return new ExpressionResult(id, type, false); + } + + private ExpressionResult parameterExpression(ExpressionId id, guru.interlis.syntax.ParameterExpressionSyntax syntax, SymbolId owner) { + List errors = new ArrayList<>(); + var target = names.bindTarget(index.byId(owner).orElseThrow(), syntax.parameter(), Set.of(SymbolKind.PARAMETER), ReferenceUsage.EXPRESSION, errors); + if (target.isEmpty()) return failedValueExpression(id, syntax.range(), errors); + var parameter = (guru.interlis.model.ParameterSymbol) ensureSymbol.apply(index.byId(target.orElseThrow()).orElseThrow()); + boolean error = typeBindings.effectiveDescriptor(parameter.typeId()) instanceof ErrorType; + state.defineExpression(new guru.interlis.model.SymbolValueExpression(id, parameter.typeId(), + error ? guru.interlis.model.ExpressionTypeState.ERROR : guru.interlis.model.ExpressionTypeState.RESOLVED, + syntax.range(), parameter.id())); + return new ExpressionResult(id, parameter.typeId(), error); + } + + ExpressionResult failedValueExpression(ExpressionId id, SourceRange range, List errors) { + TypeId type = typeBindings.nextTypeId(); + typeBindings.defineType(new ErrorType(type, new Origin.DeclaredOrigin(range), errors)); + state.defineExpression(new guru.interlis.model.ErrorExpression(id, type, + guru.interlis.model.ExpressionTypeState.ERROR, range, List.of(), errors)); + return new ExpressionResult(id, type, true); + } + + ExpressionResult failedFunctionArgument(SourceRange range,List causes) { + return failedValueExpression(state.nextExpressionId(),range,causes); + } + + ExpressionResult blockedFunctionArgument(SourceRange range,ExpressionResult dependency,DeclInfo owner) { + var descriptor=typeBindings.effectiveDescriptor(dependency.typeId());var source=state.expression(dependency.id()).range(); + var causes=descriptor instanceof ErrorType error?error.diagnosticIds():diagnostics.snapshot().stream() + .filter(d->d.severity()==Severity.ERROR&&d.primaryRange().filter(r->r.source().equals(source.source())&&r.revision().equals(source.revision()) + &&r.startOffset()<=source.endOffset()&&r.endOffset()>=source.startOffset()).isPresent()).map(guru.interlis.diagnostics.Diagnostic::id).toList(); + if(causes.isEmpty())causes=List.of(diagnostics.emitRule(owner,"invalid actual attribute owner argument","FUNCTION_ATTRIBUTE_DEPENDENCY",range)); + return failedFunctionArgument(range,causes); + } + + private ExpressionResult enumerationConstantExpression(ExpressionId id, + EnumerationConstantExpressionSyntax syntax, + SymbolId owner) { + String text = syntax.text().toLowerCase(java.util.Locale.ROOT); + if (text.equals("#true") || text.equals("#false")) { + TypeId type = typeBindings.builtinBoolean(index.byId(owner).orElseThrow().language()); + state.defineExpression(new guru.interlis.model.LiteralExpression(id, type, + guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), + guru.interlis.model.LiteralKind.BOOLEAN, syntax.text(), Optional.empty(), + Optional.empty())); + return new ExpressionResult(id, type, false); + } + TypeId type = typeBindings.nextTypeId(); + typeBindings.defineType(new guru.interlis.model.EnumArgumentType(type,new Origin.DeclaredOrigin(syntax.range()),false)); + state.defineExpression(new guru.interlis.model.ConstantExpression(id, type, + guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), + syntax.text())); + return new ExpressionResult(id, type, false); + } + + private ExpressionResult builtinFunctionExpression(ExpressionId id, + BuiltinFunctionExpressionSyntax syntax, + SymbolId owner) { + var signature = BuiltinFunctions.find(syntax.functionName().segments().getLast().text()).orElse(null); + var declaration = index.byId(owner).orElseThrow(); + boolean available=signature!=null && (signature.since()!=guru.interlis.model.LanguageVersion.ILI24 || declaration.language()==guru.interlis.model.LanguageVersion.ILI24); + List arguments=new ArrayList<>(functions.arguments(available?builtinDeclaration(signature,declaration.language()):null, + syntax.arguments(),declaration,available?signature.rule():BuiltinFunctions.Rule.SIMPLE)); + boolean error = arguments.stream().anyMatch(ExpressionResult::error); + TypeId type; + if (signature == null || signature.since() == guru.interlis.model.LanguageVersion.ILI24 + && declaration.language() != guru.interlis.model.LanguageVersion.ILI24) { + DiagnosticId cause = diagnostics.emitRule(declaration, "Unknown standard function INTERLIS." + syntax.functionName().segments().getLast().text(), + "BUILTIN_NAME"); + type = typeBindings.nextTypeId(); + typeBindings.defineType(new ErrorType(type, new Origin.DeclaredOrigin(syntax.range()), List.of(cause))); + error = true; + + } else { + type = builtinResultType(signature.result(), declaration.language()); + if (arguments.size() != signature.arguments().size()) { + diagnostics.emitTypeMismatch(ownerSource(owner), syntax.range(), + arguments.size() + " arguments", signature.arguments().size() + " arguments"); + error = true; + } + for (int i = 0; i < Math.min(arguments.size(), signature.arguments().size()); i++) { + work.checkpoint(); + var actual = arguments.get(i); + var expected = signature.arguments().get(i); + if (!actual.error() && expected!=BuiltinFunctions.Value.ATTRIBUTE + && !(i==1 && (signature.rule()==BuiltinFunctions.Rule.AREAS_ATTRIBUTE || signature.rule()==BuiltinFunctions.Rule.AREAS_TEXT) + && state.expression(actual.id()) instanceof guru.interlis.model.UndefinedExpression) + && !builtinArgumentMatches(actual.typeId(), expected, declaration.language())) { + diagnostics.emitTypeMismatch(ownerSource(owner), syntax.arguments().get(i).range(), + typeBindings.typeName(actual.typeId()), expected.name()); + error = true; + } + } + } + if(!error && (signature.name().equals("isEnumSubVal") || signature.name().equals("inEnumRange"))) + error=!enumArgumentsValid(arguments,signature,declaration,syntax.range()); + if (!error && syntax.functionName().segments().getLast().text().equals("myClass")) { + TypeDescriptor argument = typeBindings.effectiveDescriptor(arguments.getFirst().typeId()); + List targets = argument instanceof ReferenceType r ? r.targetRoots() + : argument instanceof guru.interlis.model.ObjectArgumentType o ? (o.restrictions().isEmpty() ? o.base().stream().toList() : o.restrictions()) + : argument instanceof StructureValueType structure ? structure.targetRoots() : List.of(); + typeBindings.replaceType(new ClassValueType(type, new Origin.DeclaredOrigin(syntax.range()), + argument instanceof StructureValueType || guru.interlis.model.internal.TypeCompatibility.isUndefined(argument), targets)); + } + if (!error && signature.rule()==BuiltinFunctions.Rule.CONVERT_UNIT) { + var from=typeBindings.effectiveDescriptor(arguments.getFirst().typeId()); + type=typeBindings.nextTypeId(); + typeBindings.defineType(new NumericType(type,new Origin.DeclaredOrigin(syntax.range()),new NumericRange(Optional.empty(),Optional.empty(),false, + guru.interlis.model.NumericDirection.NONE,from instanceof NumericType n?n.range().unit():Optional.empty(),Optional.empty()))); + } + FunctionBindings.Result bound = FunctionBindings.Result.empty(); + if (!error) { + bound = functions.bind(builtinDeclaration(signature,declaration.language()),arguments,declaration,syntax.range(),signature.rule()); + error = bound.error(); + } + if(available)references.recordReference(declaration,syntax.functionName(), + builtinDeclaration(signature,declaration.language()).id(),ReferenceUsage.EXPRESSION); + state.defineExpression(new guru.interlis.model.FunctionCallExpression(id, type, + error ? guru.interlis.model.ExpressionTypeState.ERROR + : guru.interlis.model.ExpressionTypeState.RESOLVED, + syntax.range(), arguments.stream().map(ExpressionResult::id).toList(), + "INTERLIS." + syntax.functionName().segments().getLast().text(), Optional.ofNullable(available?builtinDeclaration(signature,declaration.language()).id():null),bound.paths())); + return new ExpressionResult(id, type, error); + } + + private boolean enumArgumentsValid(List arguments, BuiltinFunctions.Signature signature, DeclInfo owner, SourceRange range) { + var anchor=arguments.stream().filter(a->!isEnumConstant(a)).map(a->typeBindings.effectiveDescriptor(a.typeId())) + .filter(a->a instanceof EnumerationType || a instanceof guru.interlis.model.BooleanType || a instanceof guru.interlis.model.AlignmentType).findFirst().orElse(null); + if(anchor==null)return true; // Polymorphic literal-only calls have no declared enum identity. + for(int i=0;iv.startsWith(path+"."))); + } else valid=anchor instanceof EnumerationType a && actual instanceof EnumerationType b + && guru.interlis.model.internal.TypeCompatibility.relatedEnumerations(a,b,typeBindings::effectiveDescriptor) + || anchor instanceof guru.interlis.model.BooleanType && actual instanceof guru.interlis.model.BooleanType + || anchor instanceof guru.interlis.model.AlignmentType a && actual instanceof guru.interlis.model.AlignmentType b && a.alignment()==b.alignment(); + if(guru.interlis.model.internal.TypeCompatibility.isUndefined(actual))continue; + if(!valid) {diagnostics.emitRule(owner,"enumeration function arguments must refer to compatible values of the same enumeration","ENUM_ARGUMENT",range);return false;} + if(isEnumConstant(argument)) { + TypeId context=anchor.id(); + if(anchor instanceof EnumerationType enumeration && !enumeration.treeValues() + && signature.arguments().get(i)==BuiltinFunctions.Value.ENUM_TREE) { + context=typeBindings.nextTypeId();typeBindings.defineType(new EnumerationType(context,enumeration.origin(),enumeration.values(),enumeration.ordered(), + enumeration.circular(),enumeration.finalSubtrees(),Optional.of(enumeration.id()),true)); + } + arguments.set(i,contextualEnumConstant(argument,context,owner.id())); + } + + } + return true; + } + + private List enumerationPaths(TypeDescriptor descriptor) { + if(descriptor instanceof EnumerationType enumeration)return enumeration.values().stream().map(EnumerationValue::path).toList(); + if(descriptor instanceof guru.interlis.model.BooleanType)return List.of("false","true"); + if(descriptor instanceof guru.interlis.model.AlignmentType alignment)return alignment.alignment()==guru.interlis.model.AlignmentKind.HORIZONTAL + ?List.of("Left","Center","Right"):List.of("Top","Cap","Half","Base","Bottom"); + return List.of(); + } + + FunctionSymbol builtinDeclaration(BuiltinFunctions.Signature signature, guru.interlis.model.LanguageVersion language) { + var id=new SymbolId("predefined:"+language+":function:INTERLIS."+signature.name()); + if(state.symbol(id) instanceof FunctionSymbol function)return function; + var parameters=new ArrayList(); + var names=BuiltinFunctions.parameterNames(signature.name()); + for(int i=0;i new NumericType(id, origin, NumericRange.unbounded()); + case CLASS_VALUE -> new ClassValueType(id, origin, true, List.of()); + case POLYLINE, MULTIPOLYLINE, SURFACE, MULTISURFACE -> new GeometryType(id,origin, + guru.interlis.model.GeometryKind.valueOf(value.name()),List.of(),Optional.empty(),Optional.empty(),false,false,Optional.empty()); + case ENUM, ENUM_TREE -> new guru.interlis.model.EnumArgumentType(id,origin,value==BuiltinFunctions.Value.ENUM_TREE); + case OBJECT -> new guru.interlis.model.ObjectArgumentType(id,origin,Optional.empty(),List.of(),false); + case OBJECTS -> new guru.interlis.model.ObjectArgumentType(id, origin, Optional.empty(), List.of(), true); + case ANYSTRUCTURE -> new StructureValueType(id,origin,Optional.empty(),List.of()); + case BAG -> new CollectionType(id,origin,guru.interlis.model.CollectionKind.BAG,Cardinality.unboundedFrom(0), + builtinResultType(BuiltinFunctions.Value.ANYSTRUCTURE,language)); + case TEXT, MTEXT -> new TextType(id, origin, + value == BuiltinFunctions.Value.TEXT ? TextKind.TEXT : TextKind.MTEXT, + java.util.OptionalInt.empty()); + default -> throw new IllegalArgumentException("unsupported builtin result " + value); + }; + typeBindings.defineType(descriptor); + return id; + } + + private boolean builtinArgumentMatches(TypeId actual, BuiltinFunctions.Value expected, guru.interlis.model.LanguageVersion language) { + return guru.interlis.model.internal.TypeCompatibility.assign(typeBindings.effectiveDescriptor(actual), + typeBindings.effectiveDescriptor(builtinResultType(expected,language)),typeBindings::effectiveDescriptor,state::baseOf, + domain->typeBindings.effectiveDescriptor(state.declaredType(domain)),guru.interlis.model.AssignmentContext.FUNCTION_ARGUMENT) + .decision()==guru.interlis.model.RelationDecision.ALLOWED; + } + + private ExpressionResult functionCallExpression(ExpressionId id, + FunctionCallExpressionSyntax syntax, + SymbolId owner) { + var selected=names.resolveName(index.byId(owner).orElseThrow(),syntax.functionName(),Set.of(SymbolKind.FUNCTION),true); + if(selected.target().map(state::symbol).filter(f->f instanceof FunctionSymbol && f.origin() instanceof Origin.PredefinedOrigin).isPresent()) + return builtinFunctionExpression(id,new BuiltinFunctionExpressionSyntax(syntax.id(),syntax.range(),syntax.functionName(),syntax.arguments()),owner); + List bindingErrors = new ArrayList<>(); + Optional target = names.bindTarget(index.byId(owner).orElseThrow(), syntax.functionName(), + Set.of(SymbolKind.FUNCTION), ReferenceUsage.EXPRESSION, bindingErrors); + FunctionSymbol function = target.flatMap(index::byId).map(ensureSymbol::apply) + .filter(FunctionSymbol.class::isInstance).map(FunctionSymbol.class::cast).orElse(null); + List arguments=new ArrayList<>(functions.arguments(function,syntax.arguments(),index.byId(owner).orElseThrow(),BuiltinFunctions.Rule.SIMPLE)); + List children = arguments.stream().map(ExpressionResult::id).toList(); + boolean error = arguments.stream().anyMatch(ExpressionResult::error); + TypeId type; + if (function == null) { + work.checkpoint(); + type = typeBindings.nextTypeId(); + typeBindings.defineType(new ErrorType(type, new Origin.DeclaredOrigin(syntax.range()), + bindingErrors)); + error = true; + } else { + type = function.returnType(); + if (arguments.size() != function.parameters().size()) { + diagnostics.emitTypeMismatch(ownerSource(owner), syntax.range(), + arguments.size() + " arguments", function.parameters().size() + " arguments"); + error = true; + } + for (int i = 0; i < Math.min(arguments.size(), function.parameters().size()); i++) { + work.checkpoint(); + var actual = arguments.get(i); + TypeId expected = function.parameters().get(i).typeId(); + actual=contextualValue(actual,expected,owner);arguments.set(i,actual); + if (!actual.error() && !(typeBindings.effectiveDescriptor(expected) instanceof guru.interlis.model.AttributePathType) && guru.interlis.model.internal.TypeCompatibility.assign( + typeBindings.effectiveDescriptor(actual.typeId()), typeBindings.effectiveDescriptor(expected), + typeBindings::effectiveDescriptor, state::baseOf, domain -> typeBindings.effectiveDescriptor(state.declaredType(domain)),guru.interlis.model.AssignmentContext.FUNCTION_ARGUMENT).decision() + != guru.interlis.model.RelationDecision.ALLOWED) { + diagnostics.emitTypeMismatch(ownerSource(owner), syntax.arguments().get(i).range(), + typeBindings.typeName(actual.typeId()), typeBindings.typeName(expected)); + error = true; + } + } + } + if (!error) error = functions.bind(function,arguments,index.byId(owner).orElseThrow(),syntax.range(),BuiltinFunctions.Rule.SIMPLE).error(); + if (!error) type=functions.returnType(function,arguments,syntax.range()); + state.defineExpression(new guru.interlis.model.FunctionCallExpression(id, type, + error ? guru.interlis.model.ExpressionTypeState.ERROR + : guru.interlis.model.ExpressionTypeState.RESOLVED, + syntax.range(), children, syntax.functionName().text(), target,Map.of())); + return new ExpressionResult(id, type, error); + } + + private ExpressionResult literalExpression(ExpressionId id, LiteralExpressionSyntax syntax, SymbolId owner) { + TypeId type; + TypedExpression expression; + if (syntax.literalKind() == guru.interlis.model.LiteralKind.TEXT) { + String value = InterlisStrings.decode(syntax.lexeme()).text(); + type = typeBindings.nextTypeId(); + typeBindings.defineType(new TextType(type, new Origin.DeclaredOrigin(syntax.range()), + value.indexOf('\r')>=0 || value.indexOf('\n')>=0 || value.indexOf('\t')>=0 ? TextKind.MTEXT : TextKind.TEXT, + java.util.OptionalInt.of(TextLengths.canonicalLength(value)))); + expression = new guru.interlis.model.LiteralExpression(id, type, + guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), + syntax.literalKind(), syntax.lexeme(), Optional.empty(), + Optional.of(value)); + } else { + type = numericLiteralType(syntax.decimal(), syntax.unit(), syntax.range(), owner); + expression = new guru.interlis.model.LiteralExpression(id, type, + typeBindings.effectiveDescriptor(type) instanceof ErrorType ? guru.interlis.model.ExpressionTypeState.ERROR : guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), + syntax.literalKind(), syntax.lexeme(), syntax.decimal(), Optional.empty()); + } + state.defineExpression(expression); + return new ExpressionResult(id, type, typeBindings.effectiveDescriptor(type) instanceof ErrorType); + } + + private TypeId numericLiteralType(Optional decimal, Optional unitName, SourceRange sourceRange, SymbolId owner) { + TypeId type = typeBindings.nextTypeId(); + Optional unit = unitName.flatMap(name -> units.bindUnit(index.byId(owner).orElseThrow(), name)); + if (unitName.isPresent() && unit.isEmpty()) { + var diagnostic = diagnostics.emitRule(index.byId(owner).orElseThrow(), "Numeric constant requires a known unit", "CONSTANT_UNIT", unitName.orElseThrow().range()); + typeBindings.defineType(new ErrorType(type, new Origin.DeclaredOrigin(sourceRange), List.of(diagnostic))); + return type; + } + if (unit.isPresent() && units.extensionUnit(unit.orElseThrow()).abstractUnit()) { + var diagnostic = diagnostics.emitRule(index.byId(owner).orElseThrow(), "Numeric constant requires a concrete unit", "CONSTANT_UNIT", unitName.orElseThrow().range()); + typeBindings.defineType(new ErrorType(type, new Origin.DeclaredOrigin(sourceRange), List.of(diagnostic))); + return type; + } + NumericRange range = new NumericRange(decimal, decimal, false, + guru.interlis.model.NumericDirection.NONE, unit, Optional.empty()); + typeBindings.defineType(new NumericType(type, new Origin.DeclaredOrigin(sourceRange), range)); + return type; + } + + private ExpressionResult binaryExpression(ExpressionId id, BinaryExpressionSyntax syntax, + SymbolId owner) { + ExpressionResult left = buildExpression(syntax.left(), owner); + ExpressionResult right = buildExpression(syntax.right(), owner); + TypeId result = typeBindings.builtinBoolean(index.byId(owner).orElseThrow().language()); + boolean error = left.error() || right.error(); + switch (syntax.operator()) { + case AND, OR, IMPLIES -> { + if (!error && (!typeBindings.isBoolean(left.typeId()) || !typeBindings.isBoolean(right.typeId()))) { + diagnostics.emitTypeMismatch(ownerSource(owner), syntax.range(), + typeBindings.typeName(left.typeId()) + "," + typeBindings.typeName(right.typeId()), "BOOLEAN"); + error = true; + } + } + case EQ, NEQ, LT, LTE, GT, GTE -> { + var comparison=syntax.operator()==guru.interlis.model.BinaryOperator.EQ || syntax.operator()==guru.interlis.model.BinaryOperator.NEQ + ?guru.interlis.model.ComparisonOperator.EQUALITY:guru.interlis.model.ComparisonOperator.ORDERING; + left=contextualNumericConstant(left,right.typeId()); + right=contextualNumericConstant(right,left.typeId()); + left=contextualValue(left,right.typeId(),owner); + right=contextualValue(right,left.typeId(),owner); + error |= left.error() || right.error(); + if(!error && !comparisonAllowed(left,right,comparison)) { + diagnostics.emitTypeMismatch(ownerSource(owner),syntax.range(),typeBindings.typeName(left.typeId())+","+typeBindings.typeName(right.typeId()),"COMPARABLE "+comparison); + error=true; + } + } + case PLUS, MINUS, MUL, DIV, POW -> { + result = left.typeId(); + if (!error && (!typeBindings.isNumeric(left.typeId()) || !typeBindings.isNumeric(right.typeId()))) { + diagnostics.emitTypeMismatch(ownerSource(owner), syntax.range(), + typeBindings.typeName(left.typeId()) + "," + typeBindings.typeName(right.typeId()), "NUMERIC"); + error = true; + } + } + } + TypedExpression expression = new guru.interlis.model.BinaryExpression(id, result, + error ? guru.interlis.model.ExpressionTypeState.ERROR + : guru.interlis.model.ExpressionTypeState.RESOLVED, + syntax.range(), List.of(left.id(), right.id()), syntax.operator(), + left.id(), right.id()); + state.defineExpression(expression); + return new ExpressionResult(id, result, error); + } + + private ExpressionResult unaryExpression(ExpressionId id, UnaryExpressionSyntax syntax, + SymbolId owner) { + ExpressionResult operand = buildExpression(syntax.operand(), owner); + TypeId type = typeBindings.builtinBoolean(index.byId(owner).orElseThrow().language()); + boolean error = operand.error(); + if (!error && syntax.operator() == guru.interlis.model.UnaryOperator.NOT && !typeBindings.isBoolean(operand.typeId())) { + diagnostics.emitTypeMismatch(ownerSource(owner), syntax.range(), typeBindings.typeName(operand.typeId()), + "BOOLEAN"); + error = true; + } + TypedExpression expression = new guru.interlis.model.UnaryExpression(id, type, + error ? guru.interlis.model.ExpressionTypeState.ERROR + : guru.interlis.model.ExpressionTypeState.RESOLVED, + syntax.range(), List.of(operand.id()), syntax.operator(), operand.id()); + state.defineExpression(expression); + return new ExpressionResult(id, type, error); + } + + private ExpressionResult definedExpression(ExpressionId id, DefinedExpressionSyntax syntax, + SymbolId owner) { + ExpressionResult operand = buildExpression(syntax.operand(), owner); + TypeId type = typeBindings.builtinBoolean(index.byId(owner).orElseThrow().language()); + TypedExpression expression = new guru.interlis.model.DefinedExpression(id, type, + operand.error() ? guru.interlis.model.ExpressionTypeState.ERROR + : guru.interlis.model.ExpressionTypeState.RESOLVED, + syntax.range(), List.of(operand.id()), operand.id()); + state.defineExpression(expression); + return new ExpressionResult(id, type, operand.error()); + } + + private ExpressionResult constantExpression(ExpressionId id, ConstantExpressionSyntax syntax, SymbolId owner) { + TypeId type = numericLiteralType(Optional.empty(), syntax.unit(), syntax.range(), owner); + TypedExpression expression = new guru.interlis.model.ConstantExpression(id, type, + typeBindings.effectiveDescriptor(type) instanceof ErrorType ? guru.interlis.model.ExpressionTypeState.ERROR : guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), + syntax.constantName()); + state.defineExpression(expression); + return new ExpressionResult(id, type, typeBindings.effectiveDescriptor(type) instanceof ErrorType); + } + + private ExpressionResult undefinedExpression(ExpressionId id, UndefinedExpressionSyntax syntax, SymbolId owner) { + TypeId type = typeBindings.nextTypeId(); + typeBindings.defineType(new guru.interlis.model.GenericType(type, + new Origin.PredefinedOrigin(index.byId(owner).orElseThrow().language(), "UNDEFINED"), + "UNDEFINED")); + TypedExpression expression = new guru.interlis.model.UndefinedExpression(id, type, + guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of()); + state.defineExpression(expression); + return new ExpressionResult(id, type, false); + } + + private ExpressionResult errorExpression(SourceId source, SourceRange range, String feature) { + ExpressionId id = state.nextExpressionId(); + TypeId type = typeBindings.errorType(source, range, feature); + DiagnosticId diagnostic = diagnostics.emitUnsupported(source, range, feature, feature); + TypedExpression expression = new ErrorExpression(id, type, + guru.interlis.model.ExpressionTypeState.ERROR, range, List.of(), + List.of(diagnostic)); + state.defineExpression(expression); + return new ExpressionResult(id, type, true); + } + + private boolean formattedTextValid(ExpressionResult left, ExpressionResult right) { + TypeDescriptor leftDescriptor = typeBindings.effectiveDescriptor(left.typeId()); + TypeDescriptor rightDescriptor = typeBindings.effectiveDescriptor(right.typeId()); + guru.interlis.model.FormattedType formatted; + ExpressionId textId; + if (leftDescriptor instanceof guru.interlis.model.FormattedType f) { + formatted = f; + textId = right.id(); + } else if (rightDescriptor instanceof guru.interlis.model.FormattedType f) { + formatted = f; + textId = left.id(); + } else { + return true; + } + TypedExpression expression = state.expression(textId); + if (!(expression instanceof guru.interlis.model.LiteralExpression literal) + || literal.textValue().isEmpty()) { + return true; + } + String value = literal.textValue().orElseThrow(); + return formats.formattedValues().parse(formatted, value, true).isPresent(); + } + + private boolean isEnumTreeType(TypeId id) { + return typeBindings.effectiveDescriptor(id) instanceof EnumerationType enumeration && enumeration.treeValues(); + } + + private boolean isEnumConstant(ExpressionResult result) { + TypedExpression expression = state.expression(result.id()); + return expression instanceof guru.interlis.model.ConstantExpression constant + && constant.constantName().startsWith("#"); + } + + private boolean enumConstantValid(ExpressionResult left, ExpressionResult right) { + ExpressionResult enumSide = null; + ExpressionResult constSide = null; + if (typeBindings.effectiveDescriptor(left.typeId()) instanceof EnumerationType) { + enumSide = left; + constSide = right; + } else if (typeBindings.effectiveDescriptor(right.typeId()) instanceof EnumerationType) { + enumSide = right; + constSide = left; + } + if (enumSide == null || constSide == null) { + return true; + } + TypedExpression constant = state.expression(constSide.id()); + if (!(constant instanceof guru.interlis.model.ConstantExpression value) + || !value.constantName().startsWith("#")) { + return true; + } + String path = value.constantName().substring(1); + EnumerationType enumeration = (EnumerationType) typeBindings.effectiveDescriptor(enumSide.typeId()); + boolean exact = enumeration.values().stream().anyMatch(v -> v.path().equals(path)); + boolean hasChildren = enumeration.values().stream().anyMatch(v -> v.path().startsWith(path + ".")); + return exact && (!hasChildren || isEnumTreeType(enumSide.typeId())); + } + + /** Bind constants to their expected value type; shared inherited expressions are only checked by valueAssignable. */ + ExpressionResult contextualValue(ExpressionResult value, TypeId expected, SymbolId owner) { + if(value.error())return value; + value=contextualNumericConstant(value,expected); + if (state.expression(value.id()) instanceof guru.interlis.model.FunctionCallExpression call && isUnitConversion(call) + && typeBindings.effectiveDescriptor(expected) instanceof NumericType numeric) { + // Unrestricted NUMERIC is a wildcard formal, not a new unitless destination. + if(numeric.range().unit().isEmpty() && numeric.range().minimum().isEmpty() && numeric.range().maximum().isEmpty())return value; + TypeId type=expected;boolean error=!conversionCompatible(call,numeric); + if(error) { + var diagnostic=diagnostics.emitRule(index.byId(owner).orElseThrow(),"convertUnit source and destination need a common conversion unit","CONVERT_UNIT",call.range()); + type=typeBindings.nextTypeId();typeBindings.defineType(new ErrorType(type,new Origin.DeclaredOrigin(call.range()),List.of(diagnostic))); + } + state.replaceExpression(new guru.interlis.model.FunctionCallExpression(call.id(),type, + error?guru.interlis.model.ExpressionTypeState.ERROR:call.typeState(),call.range(),call.children(),call.functionName(),call.targetFunction(),call.argumentPaths())); + return new ExpressionResult(call.id(),type,error); + } + var result=contextualEnumConstant(value,expected,owner); + if(state.expression(result.id()) instanceof guru.interlis.model.LiteralExpression literal && literal.textValue().isPresent() + && typeBindings.effectiveDescriptor(expected) instanceof FormattedType formatted + && formats.formattedValues().parse(formatted,literal.textValue().orElseThrow(),true).isPresent()) { + state.replaceExpression(new guru.interlis.model.LiteralExpression(literal.id(),expected,literal.typeState(),literal.range(),literal.children(),literal.literalKind(),literal.lexeme(),literal.decimal(),literal.textValue())); + return new ExpressionResult(result.id(),expected,false); + } + return result; + } + + private ExpressionResult contextualNumericConstant(ExpressionResult value, TypeId expected) { + if(value.error() || !(typeBindings.effectiveDescriptor(expected) instanceof NumericType target) || target.range().unit().isEmpty() + || !(typeBindings.effectiveDescriptor(value.typeId()) instanceof NumericType source) || source.range().unit().isPresent())return value; + var expression=state.expression(value.id()); + if(!(expression instanceof guru.interlis.model.LiteralExpression literal && literal.literalKind()==guru.interlis.model.LiteralKind.NUMBER) + && !(expression instanceof guru.interlis.model.ConstantExpression constant && (constant.constantName().equals("PI") || constant.constantName().equals("LNBASE"))))return value; + TypeId type=typeBindings.nextTypeId();var range=source.range(); + typeBindings.defineType(new NumericType(type,source.origin(),new NumericRange(range.minimum(),range.maximum(),range.circular(),range.direction(),target.range().unit(),range.referenceSystem()))); + if(expression instanceof guru.interlis.model.LiteralExpression literal) + state.replaceExpression(new guru.interlis.model.LiteralExpression(literal.id(),type,literal.typeState(),literal.range(),literal.children(),literal.literalKind(),literal.lexeme(),literal.decimal(),literal.textValue())); + else {var constant=(guru.interlis.model.ConstantExpression)expression; + state.replaceExpression(new guru.interlis.model.ConstantExpression(constant.id(),type,constant.typeState(),constant.range(),constant.children(),constant.constantName()));} + return new ExpressionResult(value.id(),type,false); + } + + private boolean isUnitConversion(guru.interlis.model.FunctionCallExpression call) { + return call.targetFunction().flatMap(id->Optional.ofNullable(state.symbol(id))).filter(FunctionSymbol.class::isInstance) + .map(Symbol::origin).filter(Origin.PredefinedOrigin.class::isInstance).map(Origin.PredefinedOrigin.class::cast) + .map(origin->origin.key().equals("INTERLIS.convertUnit")).orElse(false); + } + + private boolean conversionCompatible(guru.interlis.model.FunctionCallExpression call,NumericType target) { + var descriptor=typeBindings.effectiveDescriptor(state.expression(call.children().getFirst()).typeId()); + if(guru.interlis.model.internal.TypeCompatibility.isUndefined(descriptor))return true; + var from=(NumericType)descriptor; + var sourceUnit=from.range().unit();var targetUnit=target.range().unit(); + if(sourceUnit.isEmpty() || targetUnit.isEmpty())return sourceUnit.isEmpty() && targetUnit.isEmpty(); + return guru.interlis.model.internal.TypeCompatibility.unitRelation(sourceUnit.orElseThrow(),targetUnit.orElseThrow(),units::extensionUnit) + .map(relation->relation.decision()==guru.interlis.model.RelationDecision.ALLOWED).orElse(false); + } + + boolean valueAssignable(TypedExpression value, TypeId expected) { + var descriptor=typeBindings.effectiveDescriptor(expected); + if(value instanceof guru.interlis.model.FunctionCallExpression call && isUnitConversion(call) && descriptor instanceof NumericType numeric) + return conversionCompatible(call,numeric); + if(value instanceof guru.interlis.model.ConstantExpression constant && constant.constantName().startsWith("#")) { + var paths=enumerationPaths(descriptor);String path=constant.constantName().substring(1); + if(!paths.isEmpty())return paths.contains(path) && (!(descriptor instanceof EnumerationType enumeration) || enumeration.treeValues() + || paths.stream().noneMatch(p->p.startsWith(path+"."))); + } + if(value instanceof guru.interlis.model.LiteralExpression literal && literal.textValue().isPresent() && descriptor instanceof FormattedType formatted) + return formats.formattedValues().parse(formatted,literal.textValue().orElseThrow(),true).isPresent(); + return typeBindings.assignable(value.typeId(),expected); + } + + private ExpressionResult contextualEnumConstant(ExpressionResult value, TypeId context, SymbolId owner) { + if(!isEnumConstant(value))return value; + var actual=(guru.interlis.model.ConstantExpression)state.expression(value.id()); + TypeDescriptor expected=typeBindings.effectiveDescriptor(context); + String path=actual.constantName().substring(1); + var paths=enumerationPaths(expected); + boolean valid=paths.contains(path) && (!(expected instanceof EnumerationType e) || e.treeValues() + || paths.stream().noneMatch(p->p.startsWith(path+"."))); + if(!valid)return value; + state.replaceExpression(new guru.interlis.model.ConstantExpression(actual.id(),context,actual.typeState(),actual.range(),actual.children(),actual.constantName())); + if(expected instanceof EnumerationType enumeration) { + var segments=new ArrayList();int start=actual.range().startOffset()+1;String prefix=""; + for(String part:path.split("\\.")) { + prefix=prefix.isEmpty()?part:prefix+"."+part; + var target=enumMember(enumeration,prefix); + var range=new SourceRange(actual.range().source(),actual.range().revision(),start,start+part.length()); + segments.add(new ReferenceSegment(part,range,Optional.ofNullable(target)));start+=part.length()+1; + } + SymbolId target=segments.getLast().target().orElse(null); + var range=new SourceRange(actual.range().source(),actual.range().revision(),actual.range().startOffset()+1,actual.range().endOffset()); + references.recordEnumReference(owner, range, segments, target); + } + return new ExpressionResult(value.id(),context,value.error()); + } + + private SymbolId enumMember(EnumerationType type,String path) { + Set seen=new HashSet<>(); + for(var current=type;current!=null && seen.add(current.id());) { + for(var entry:state.declaredTypesInBuildOrder()) { + work.checkpoint();TypeDescriptor candidate=typeBindings.effectiveDescriptor(entry.getValue()); + while(candidate instanceof CollectionType collection)candidate=typeBindings.effectiveDescriptor(collection.elementType()); + if(candidate==null || !candidate.id().equals(current.id()))continue; + for(var child:index.children(entry.getKey()))if(child.kind()==SymbolKind.ENUM_ELEMENT && child.name().equals(path))return child.id(); + } + var base=current.baseType().map(typeBindings::effectiveDescriptor).orElse(null);current=base instanceof EnumerationType e?e:null; + } + return null; + } + + private boolean comparisonAllowed(ExpressionResult left, ExpressionResult right, guru.interlis.model.ComparisonOperator operator) { + if(!enumConstantValid(left,right) || !formattedTextValid(left,right))return false; + TypeDescriptor a=typeBindings.effectiveDescriptor(left.typeId()),b=typeBindings.effectiveDescriptor(right.typeId()); + if(isEnumConstant(left) && b instanceof EnumerationType)a=b; + if(isEnumConstant(right) && a instanceof EnumerationType)b=a; + if(a instanceof FormattedType && state.expression(right.id()) instanceof guru.interlis.model.LiteralExpression literal + && literal.literalKind()==guru.interlis.model.LiteralKind.TEXT)b=a; + if(b instanceof FormattedType && state.expression(left.id()) instanceof guru.interlis.model.LiteralExpression literal + && literal.literalKind()==guru.interlis.model.LiteralKind.TEXT)a=b; + return guru.interlis.model.internal.TypeCompatibility.compare(a,b,operator,typeBindings::effectiveDescriptor,id->typeBindings.effectiveDescriptor(state.declaredType(id))).decision() + ==guru.interlis.model.RelationDecision.ALLOWED; + } + + private Optional decimal(String lexeme) { + try { + return Optional.of(DecimalLiteral.parse(lexeme)); + } catch (RuntimeException e) { + return Optional.empty(); + } + } + + SourceId ownerSource(SymbolId owner) { + return index.modelOfSymbol(owner).flatMap(index::byId) + .map(DeclInfo::sourceId).orElseGet(() -> sourceSet.sources().keySet().iterator().next()); + } + + record ExpressionResult(ExpressionId id, TypeId typeId, boolean error) { + } + + void defineExpression(TypedExpression expression) { state.defineExpression(expression); } + + ExpressionId nextExpressionId() { return state.nextExpressionId(); } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/FormattedTypeBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/FormattedTypeBindings.java new file mode 100644 index 00000000..aea8097e --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/FormattedTypeBindings.java @@ -0,0 +1,175 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.diagnostics.DiagnosticId; +import guru.interlis.model.CollectionType; +import guru.interlis.model.ErrorType; +import guru.interlis.model.FormattedType; +import guru.interlis.model.NumericRange; +import guru.interlis.model.NumericType; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; +import guru.interlis.model.internal.FormattedValues; +import guru.interlis.syntax.FormattedTypeSyntax; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +/** Binds formatted types and checks their bound ranges and components. */ +final class FormattedTypeBindings { + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameBindings names; + private final Supplier typeBindings; + private final InheritanceBindings inheritance; + private final Supplier paths; + private final ReferenceBindings references; + private final StructureBindings structures; + + FormattedTypeBindings(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameBindings names, + Supplier typeBindings, + InheritanceBindings inheritance, + Supplier paths, + ReferenceBindings references, + StructureBindings structures) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.names = names; + this.typeBindings = typeBindings; + this.inheritance = inheritance; + this.paths = paths; + this.references = references; + this.structures = structures; + } + + private Optional bindFormatName(SymbolId context,guru.interlis.syntax.NameSyntax name,Set kinds) { + var target=names.bindTarget(index.byId(context).orElseThrow(),name,kinds,ReferenceUsage.TYPE_USE,new ArrayList<>()); + target.flatMap(index::byId).filter(d->d.kind()==SymbolKind.DOMAIN).ifPresent(typeBindings.get()::domainType); + return target; + } + + TypeDescriptor buildFormattedType(TypeId id, FormattedTypeSyntax syntax, SymbolId context) { + DeclInfo owner = index.byId(context).orElseThrow(); + List errors=new ArrayList<>(); + Optional domain = syntax.basedOnStructure() || syntax.base().isEmpty() + ? Optional.ofNullable(state.baseOf(context)) : Optional.empty(); + Optional structure = Optional.empty(); + Optional predefined = Optional.empty(); + if (syntax.base().isPresent()) { + var name = syntax.base().orElseThrow(); + if (syntax.basedOnStructure()) structure = bindFormatName(context, name, Set.of(SymbolKind.STRUCTURE)); + else domain = bindFormatName(context, name, Set.of(SymbolKind.DOMAIN)); + } + TypeDescriptor inherited = domain.map(state::declaredType).map(typeBindings.get()::effectiveDescriptor).orElse(null); + FormattedType base = inherited instanceof FormattedType f ? f : null; + List parts = new ArrayList<>(); + if (!syntax.basedOnStructure()) { + if (base == null) errors.add(diagnostics.emitRule(owner, "FORMAT requires a bound formatted base domain", "FORMAT_BASE")); + else { structure = base.structure(); predefined = base.predefined(); parts.addAll(base.parts()); } + } else if (syntax.inheritance()) { + if (base == null || structure.isEmpty() || base.structure().isEmpty() + || !inheritance.targetSpecializes(structure.orElseThrow(), base.structure().orElseThrow())) + errors.add(diagnostics.emitRule(owner, "FORMAT INHERITANCE requires the base structure or a specialization", "FORMAT_INHERITANCE")); + else for (var part : base.parts()) { + // INHERITANCE keeps the layout and provenance, but uses the effective + // attribute of the specialized structure (including its precision). + Optional attribute = part.attribute().isEmpty() ? Optional.empty() + : paths.get().findMember(structure.orElseThrow(), state.symbol(part.attribute().orElseThrow()).name().value()); + parts.add(new FormattedType.Part(attribute, part.width(), part.format(), part.literal(), part.origin())); + } + } else if (base != null) errors.add(diagnostics.emitRule(owner, "extended FORMAT must retain its inherited prefix", "FORMAT_INHERITANCE")); + for (var part : syntax.parts()) { + work.checkpoint(); + Optional attribute = Optional.empty(), nested = Optional.empty(); + if (part.attribute().isEmpty()) { + if (!part.literal().isEmpty() && Character.isDigit(part.literal().charAt(0))) + errors.add(diagnostics.emitRule(owner, "FORMAT separator must not start with a digit", "FORMAT_SEPARATOR")); + } else { + var name = part.attribute().orElseThrow(); + attribute = structure.flatMap(st -> paths.get().findMember(st, name.text())); + references.recordReference(owner, name, attribute.orElse(null), ReferenceUsage.TYPE_USE); + if (attribute.isEmpty()) errors.add(diagnostics.unresolved(name)); + attribute.flatMap(index::byId).ifPresent(typeBindings.get()::attributeType); + TypeDescriptor value = attribute.map(state::declaredType).map(typeBindings.get()::effectiveDescriptor).orElse(null); + if (part.format().isPresent()) { + nested = bindFormatName(context, part.format().orElseThrow(), Set.of(SymbolKind.DOMAIN)); + TypeDescriptor nestedType = nested.map(state::declaredType).map(typeBindings.get()::effectiveDescriptor).orElse(null); + SymbolId selected = value instanceof CollectionType c && !paths.get().isMany(c.cardinality()) ? structures.target(c) : structures.target(value); + if (value instanceof CollectionType c && paths.get().isMany(c.cardinality())) selected = null; + if (!(nestedType instanceof FormattedType f) || f.structure().isEmpty() || selected == null + || !inheritance.targetSpecializes(selected, f.structure().orElseThrow())) + errors.add(diagnostics.emitRule(owner, "nested FORMAT requires a compatible singleton structure", "FORMAT_ATTRIBUTE")); + } else if (!(value instanceof NumericType)) errors.add(diagnostics.emitRule(owner, "FORMAT attribute must be numeric", "FORMAT_ATTRIBUTE")); + else { + NumericRange range = ((NumericType)value).range(); + if (range.minimum().isPresent() && range.maximum().isPresent() + && range.minimum().orElseThrow().precision().fractionalDigits() + != range.maximum().orElseThrow().precision().fractionalDigits()) + errors.add(diagnostics.emitRule(owner, "FORMAT component bounds must have matching precision", "FORMAT_PRECISION")); + } + if (part.width().filter(w -> w.signum() <= 0).isPresent()) errors.add(diagnostics.emitRule(owner, "FORMAT width must be positive", "FORMAT_WIDTH")); + } + parts.add(new FormattedType.Part(attribute, part.width(), nested, part.literal(), new Origin.DeclaredOrigin(part.range()))); + } + var result = new FormattedType(id, new Origin.DeclaredOrigin(syntax.range()), + syntax.base().map(guru.interlis.syntax.NameSyntax::text).orElse(""), + predefined, domain, structure, parts, syntax.minimum(), syntax.maximum()); + if (result.minimum().isPresent() || result.maximum().isPresent()) { + var min = result.minimum().flatMap(v -> formatValue(result, v)); + var max = result.maximum().flatMap(v -> formatValue(result, v)); + if (min.isEmpty() || max.isEmpty() || FormattedValues.compare(min.orElseThrow(), max.orElseThrow()) > 0) + errors.add(diagnostics.emitRule(owner, "FORMAT bounds must fit the layout and component ranges", "FORMAT_BOUNDS")); + else { + Set seen = new LinkedHashSet<>(); + for (FormattedType ancestor = base; ancestor != null && seen.add(ancestor.id());) { + work.checkpoint(); + var lower = ancestor.minimum().isPresent() ? formatValue(ancestor, ancestor.minimum().orElseThrow()) : Optional.>empty(); + var upper = ancestor.maximum().isPresent() ? formatValue(ancestor, ancestor.maximum().orElseThrow()) : Optional.>empty(); + if (lower.filter(v -> FormattedValues.compare(min.orElseThrow(), v) < 0).isPresent() + || upper.filter(v -> FormattedValues.compare(max.orElseThrow(), v) > 0).isPresent()) + errors.add(diagnostics.emitRule(owner, "FORMAT extension widens base bounds", "FORMAT_BOUNDS")); + TypeDescriptor next = ancestor.baseDomain().map(state::declaredType).map(typeBindings.get()::effectiveDescriptor).orElse(null); + ancestor = next instanceof FormattedType f ? f : null; + } + } + } + return errors.isEmpty()?result:new ErrorType(id,new Origin.DeclaredOrigin(syntax.range()),errors); + } + + void validateFormattedUses() { + for (var entry : state.declaredTypesInBuildOrder()) { + work.checkpoint(); + DeclInfo declaration = index.byId(entry.getKey()).orElse(null); + if (declaration == null || declaration.kind() != SymbolKind.DOMAIN && declaration.kind() != SymbolKind.ATTRIBUTE) + continue; + TypeDescriptor type = typeBindings.get().effectiveDescriptor(entry.getValue()); + while (type instanceof CollectionType collection) type = typeBindings.get().effectiveDescriptor(collection.elementType()); + if (type instanceof FormattedType format && formattedValues().isAbstract(format) + && !declaration.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) + diagnostics.emitRule(declaration, "FORMAT with abstract components requires an abstract domain or attribute", "FORMAT_ABSTRACT"); + } + } + + FormattedValues formattedValues() { + return new FormattedValues(id -> typeBindings.get().effectiveDescriptor(state.declaredType(id)), work::checkpoint); + } + + private Optional> formatValue(FormattedType type, String value) { + return formattedValues().parse(type, value, false); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/FunctionBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/FunctionBindings.java index 3fc41dc7..baed0c3a 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/FunctionBindings.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/FunctionBindings.java @@ -3,13 +3,40 @@ import guru.interlis.model.*; import guru.interlis.source.SourceRange; import guru.interlis.syntax.*; + import java.util.*; +import java.util.function.BiPredicate; +import java.util.function.Supplier; /** Dependent function arguments within one compiler run. Uses the common type * and path binders; owns no second graph, source parser or name semantics. */ final class FunctionBindings { - private final GraphBuilder g; - FunctionBindings(GraphBuilder graph) { g=graph; } + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final Supplier expressions; + private final TypeBindings typeBindings; + private final ObjectPathBindings pathBindings; + private final BiPredicate sameBoundExpression; + + FunctionBindings(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + Supplier expressions, + TypeBindings typeBindings, + ObjectPathBindings pathBindings, + BiPredicate sameBoundExpression) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.expressions = expressions; + this.typeBindings = typeBindings; + this.pathBindings = pathBindings; + this.sameBoundExpression = sameBoundExpression; + } record Result(boolean error, Map paths) { Result { paths=Map.copyOf(paths); } @@ -19,35 +46,35 @@ record Result(boolean error, Map pa /** Bind actuals in dependency order while preserving their source positions. * A formal ATTRIBUTE OF may refer to a later formal; traversal is iterative * and memoized, with no current-call state outside this invocation. */ - List arguments(FunctionSymbol function,List syntax, + List arguments(FunctionSymbol function,List syntax, DeclInfo owner,BuiltinFunctions.Rule rule) { - if(function==null)return syntax.stream().map(s->g.buildExpression(s,owner.id())).toList(); - var values=new GraphBuilder.ExpressionResult[syntax.size()]; + if(function==null)return syntax.stream().map(s->expressions.get().buildExpression(s,owner.id())).toList(); + var values=new ExpressionBindings.ExpressionResult[syntax.size()]; var arguments=Arrays.asList(values); for(int i=0;i();var active=new HashSet();int current=i; + work.checkpoint();var chain=new ArrayList();var active=new HashSet();int current=i; while(values[current]==null) { - g.checkWork(); + work.checkpoint(); if(!active.add(current)) { - var cause=g.emitRule(owner,"cyclic actual attribute argument dependency","FUNCTION_ARGUMENT_CYCLE",syntax.get(current).range()); - values[current]=g.failedFunctionArgument(syntax.get(current).range(),List.of(cause));break; + var cause=diagnostics.emitRule(owner,"cyclic actual attribute argument dependency","FUNCTION_ARGUMENT_CYCLE",syntax.get(current).range()); + values[current]=expressions.get().failedFunctionArgument(syntax.get(current).range(),List.of(cause));break; } chain.add(current);int dependency=dependency(function,current); if(dependency<0||dependency>=values.length||values[dependency]!=null)break; current=dependency; } for(int n=chain.size()-1;n>=0;n--) { - g.checkWork();int position=chain.get(n);if(values[position]!=null)continue; - var formal=position=0&&dependencyof(); values[position]=expression instanceof AttributeConstantExpressionSyntax constant&&constant.viewable().isEmpty()&&roots.size()==1 - ?g.contextualAttributeConstant(constant,owner.id(),roots.getFirst()):g.buildExpression(syntax.get(position),owner.id()); + ?expressions.get().contextualAttributeConstant(constant,owner.id(),roots.getFirst()):expressions.get().buildExpression(syntax.get(position),owner.id()); } } return List.copyOf(arguments); @@ -55,7 +82,7 @@ List arguments(FunctionSymbol function,List=function.parameters().size())return -1; - var formal=g.effectiveDescriptor(function.parameters().get(position).typeId()); + var formal=typeBindings.effectiveDescriptor(function.parameters().get(position).typeId()); return formal instanceof AttributePathType attribute&&attribute.owner().orElse(null) instanceof AttributePathType.Argument argument &&argument.function().equals(function.id())?argument.index():-1; } @@ -64,47 +91,47 @@ private int dependency(FunctionSymbol function,int position) { * only. Undefined or unrestricted object roots retain their formal symbolic * dependency, with the actual still reachable through the call's children. * The declaration stays immutable; no external function is evaluated. */ - TypeId returnType(FunctionSymbol function,List arguments,SourceRange range) { - var descriptor=g.effectiveDescriptor(function.returnType()); + TypeId returnType(FunctionSymbol function,List arguments,SourceRange range) { + var descriptor=typeBindings.effectiveDescriptor(function.returnType()); if(!(descriptor instanceof AttributePathType attribute) ||!(attribute.owner().orElse(null) instanceof AttributePathType.Argument dependency) ||!dependency.function().equals(function.id())||dependency.index()>=arguments.size())return function.returnType(); var actual=arguments.get(dependency.index()); AttributePathType.Owner owner; - if(g.effectiveDescriptor(actual.typeId()) instanceof ClassValueType) { - var value=g.expressions.get(actual.id()); + if(typeBindings.effectiveDescriptor(actual.typeId()) instanceof ClassValueType) { + var value=state.expression(actual.id()); owner=value instanceof SymbolValueExpression symbol&&Set.of(SymbolKind.CLASS,SymbolKind.STRUCTURE,SymbolKind.ASSOCIATION,SymbolKind.VIEW) - .contains(g.index.byId(symbol.target()).map(DeclInfo::kind).orElseGet(()->g.symbols.get(symbol.target()).kind())) + .contains(index.byId(symbol.target()).map(DeclInfo::kind).orElseGet(()->state.symbol(symbol.target()).kind())) ?new AttributePathType.Viewable(symbol.target()):new AttributePathType.ClassAttribute(actual.id()); } else { var roots=roots(actual.typeId(),new HashSet<>()); if(roots.size()!=1)return function.returnType(); owner=new AttributePathType.Viewable(roots.getFirst()); } - var id=g.nextTypeId(); - g.types.put(id,new AttributePathType(id,new Origin.DeclaredOrigin(range),Optional.of(owner),attribute.restrictions())); + var id=typeBindings.nextTypeId(); + typeBindings.defineType(new AttributePathType(id,new Origin.DeclaredOrigin(range),Optional.of(owner),attribute.restrictions())); return id; } private List argumentRoots(AttributePathType attribute,int position,FunctionSymbol function, - List arguments,BuiltinFunctions.Rule rule) { + List arguments,BuiltinFunctions.Rule rule) { if(rule==BuiltinFunctions.Rule.AREAS_ATTRIBUTE&&position==2&&arguments.size()>1&&arguments.get(1)!=null&&undefined(arguments.get(1))) return roots(arguments.getFirst().typeId(),new HashSet<>()); return ownerRoots(attribute.owner(),function,arguments,new HashSet<>()); } - Result bind(FunctionSymbol function,List arguments,DeclInfo owner, + Result bind(FunctionSymbol function,List arguments,DeclInfo owner, SourceRange range,BuiltinFunctions.Rule rule) { boolean error=false; var paths=new LinkedHashMap(); for(int i=0;i argument if(undefinedValue)continue; var roots=argumentRoots(attribute,i,function,arguments,rule); boolean valid=attributeMatches(actual.typeId(),attribute,roots,function,arguments); - if(!valid) { error=true;g.emitRule(owner,"attribute argument has an incompatible owner or value type","FUNCTION_ATTRIBUTE",g.expressions.get(actual.id()).range()); } + if(!valid) { error=true;diagnostics.emitRule(owner,"attribute argument has an incompatible owner or value type","FUNCTION_ATTRIBUTE",state.expression(actual.id()).range()); } } else if(rule==BuiltinFunctions.Rule.AREAS_TEXT && i>0) { int root=i==1 || undefined(arguments.get(1))?0:1; - var value=g.expressions.get(actual.id()); + var value=state.expression(actual.id()); if(!(value instanceof LiteralExpression literal) || literal.textValue().isEmpty()) { - error=true;g.emitRule(owner,"area path argument requires a constant TEXT path","FUNCTION_PATH_CONSTANT",value.range());continue; + error=true;diagnostics.emitRule(owner,"area path argument requires a constant TEXT path","FUNCTION_PATH_CONSTANT",value.range());continue; } - var fragment=g.parsePathLiteral(literal.range(),owner.language()); + var fragment=pathBindings.parsePathLiteral(literal.range(),owner.language()); if(!fragment.consumedEntireFragment() || fragment.diagnostics().stream().anyMatch(d->d.severity()==guru.interlis.diagnostics.Severity.ERROR) || !(fragment.root() instanceof PathExpressionSyntax path)) { error=true;continue; } var dependency=paths.get(root); List roots=dependency==null?roots(arguments.get(root).typeId(),new HashSet<>()) - :dependency.binding().map(g.expressions::get).map(e->roots(e.typeId(),new HashSet<>())).orElseGet(List::of); + :dependency.binding().map(state::expression).map(e->roots(e.typeId(),new HashSet<>())).orElseGet(List::of); TypeId expected=areaPathType(i,owner.language()); Optional binding=Optional.empty(); if(!roots.isEmpty()) { - SymbolId scope=g.commonObjectScope(roots); - if(scope==null) {error=true;g.emitRule(owner,"path roots have no common object context","FUNCTION_PATH_OWNER",literal.range());continue;} - var bound=g.valuePath(path,owner.id(),scope);binding=Optional.of(bound.id()); + SymbolId scope=pathBindings.commonObjectScope(roots); + if(scope==null) {error=true;diagnostics.emitRule(owner,"path roots have no common object context","FUNCTION_PATH_OWNER",literal.range());continue;} + var bound=pathBindings.valuePath(path,owner.id(),scope);binding=Optional.of(bound.id()); if(bound.error())error=true; - else if(!assign(bound.typeId(),expected)) {error=true;g.emitRule(owner,"area path has an incompatible value type","FUNCTION_PATH_TYPE",literal.range());} + else if(!assign(bound.typeId(),expected)) {error=true;diagnostics.emitRule(owner,"area path has an incompatible value type","FUNCTION_PATH_TYPE",literal.range());} } var steps=path.segments().stream().map(s->new FunctionCallExpression.ArgumentPathStep(s.name().text(),s.range(),s.associationAccess(), s.associationQualifier().map(n->new QualifiedName(n.segments().stream().map(s2->s2.text()).toList())),s.index())).toList(); @@ -145,30 +172,30 @@ Result bind(FunctionSymbol function,List argument private TypeId areaPathType(int index,LanguageVersion language) { // Reuse the ATTRIBUTE signatures of the same operation, not a second type family. var signature=BuiltinFunctions.find("areAreas").orElseThrow(); - var formal=g.builtinDeclaration(signature,language).parameters().get(index).typeId(); - return ((AttributePathType)g.effectiveDescriptor(formal)).restrictions().getFirst(); + var formal=expressions.get().builtinDeclaration(signature,language).parameters().get(index).typeId(); + return ((AttributePathType)typeBindings.effectiveDescriptor(formal)).restrictions().getFirst(); } - private boolean undefined(GraphBuilder.ExpressionResult argument) { return guru.interlis.model.internal.TypeCompatibility.isUndefined(g.effectiveDescriptor(argument.typeId())); } + private boolean undefined(ExpressionBindings.ExpressionResult argument) { return guru.interlis.model.internal.TypeCompatibility.isUndefined(typeBindings.effectiveDescriptor(argument.typeId())); } private List ownerRoots(Optional owner,FunctionSymbol function, - List arguments,Set visited) { + List arguments,Set visited) { if(owner.isEmpty())return List.of(); return switch(owner.orElseThrow()) { case AttributePathType.Viewable v -> List.of(v.target()); - case AttributePathType.ClassAttribute a -> roots(g.expressions.get(a.path()).typeId(),visited); + case AttributePathType.ClassAttribute a -> roots(state.expression(a.path()).typeId(),visited); case AttributePathType.Argument a -> a.function().equals(function.id()) && a.index() owners, - FunctionSymbol function,List arguments) { - if(!(g.effectiveDescriptor(actual) instanceof AttributePathType attribute))return false; + FunctionSymbol function,List arguments) { + if(!(typeBindings.effectiveDescriptor(actual) instanceof AttributePathType attribute))return false; boolean sameDependentValue=attribute.owner().orElse(null) instanceof AttributePathType.ClassAttribute path &&formal.owner().orElse(null) instanceof AttributePathType.Argument argument &&argument.function().equals(function.id())&&argument.index()!owner.equals(value.target()))))return false; return formal.restrictions().isEmpty() || !attribute.restrictions().isEmpty() && attribute.restrictions().stream() @@ -176,14 +203,14 @@ private boolean attributeMatches(TypeId actual,AttributePathType formal,Listg.effectiveDescriptor(g.declaredTypes.get(domain)),AssignmentContext.FUNCTION_ARGUMENT) + return guru.interlis.model.internal.TypeCompatibility.assign(typeBindings.effectiveDescriptor(source),typeBindings.effectiveDescriptor(target), + typeBindings::effectiveDescriptor,state::baseOf,domain->typeBindings.effectiveDescriptor(state.declaredType(domain)),AssignmentContext.FUNCTION_ARGUMENT) .decision()==RelationDecision.ALLOWED; } private List roots(TypeId id,Set visited) { - g.checkWork();if(!visited.add(id))return List.of(); - var type=g.effectiveDescriptor(id); + work.checkpoint();if(!visited.add(id))return List.of(); + var type=typeBindings.effectiveDescriptor(id); if(type instanceof ObjectArgumentType object)return object.restrictions().isEmpty()?object.base().stream().toList():object.restrictions(); if(type instanceof ReferenceType reference)return reference.targetRoots(); if(type instanceof StructureValueType structure)return structure.targetRoots(); diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GenericBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GenericBindings.java new file mode 100644 index 00000000..e81b0f1e --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GenericBindings.java @@ -0,0 +1,285 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.model.AttributeSymbol; +import guru.interlis.model.CollectionType; +import guru.interlis.model.CoordinateType; +import guru.interlis.model.DomainReferenceType; +import guru.interlis.model.DomainSymbol; +import guru.interlis.model.GeometryType; +import guru.interlis.model.ModelSymbol; +import guru.interlis.model.NumericType; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.StructureSymbol; +import guru.interlis.model.StructureValueType; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolHeader; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TopicSymbol; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Binds ordered generic contexts and applies their visibility rules. */ +final class GenericBindings { + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameBindings names; + private final InheritanceBindings inheritance; + private final Supplier metadata; + private final Consumer replaceContextTopic; + private final Function symbolHeader; + private final Function effectiveDescriptor; + private final Function scopeRoot; + + GenericBindings(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameBindings names, + InheritanceBindings inheritance, + Supplier metadata, + Consumer replaceContextTopic, + Function symbolHeader, + Function effectiveDescriptor, + Function scopeRoot) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.names = names; + this.inheritance = inheritance; + this.metadata = metadata; + this.replaceContextTopic = replaceContextTopic; + this.symbolHeader = symbolHeader; + this.effectiveDescriptor = effectiveDescriptor; + this.scopeRoot = scopeRoot; + } + + guru.interlis.model.ContextSymbol buildContext(DeclInfo decl) { + var syntax = (guru.interlis.syntax.ContextDeclarationSyntax) decl.syntax(); + if (index.byId(decl.owner().orElseThrow()).orElseThrow().kind() != SymbolKind.MODEL) + diagnostics.emitRule(decl, "CONTEXT must be declared at model level", "CONTEXT_SCOPE"); + if (index.children(decl.owner().orElseThrow()).stream() + .anyMatch(d -> !d.id().equals(decl.id()) && d.name().equals(decl.name()))) + diagnostics.emitRule(decl, "context name must be unique", "CONTEXT_NAME_UNIQUE"); + List mappings = new ArrayList<>(); + Set own = new HashSet<>(); + for (var mapping : syntax.mappings()) { + work.checkpoint(); + var generic = names.bindTarget(decl, mapping.genericDomain(), Set.of(SymbolKind.DOMAIN), + ReferenceUsage.TYPE_USE, new ArrayList<>()); + List alternatives = new ArrayList<>(); + for (var name : mapping.alternatives()) names.bindTarget(decl, name, Set.of(SymbolKind.DOMAIN), + ReferenceUsage.TYPE_USE, new ArrayList<>()).ifPresent(alternatives::add); + if (generic.isEmpty()) continue; + if (!own.add(generic.get())) diagnostics.emitRule(decl, "duplicate generic mapping in context", "CONTEXT_DUPLICATE"); + if (new HashSet<>(alternatives).size() != alternatives.size()) + diagnostics.emitRule(decl, "duplicate concrete alternative", "CONTEXT_DUPLICATE"); + mappings.add(new guru.interlis.model.ContextSymbol.Mapping(generic.get(), alternatives, mapping.genericDomain().range())); + } + return new guru.interlis.model.ContextSymbol(symbolHeader.apply(decl), mappings); + } + + /** Bound contexts are model-local; their effects flow only along explicit import edges. */ + void validateGenericSystem() { + for (Symbol symbol : state.symbolsInBuildOrder()) { + work.checkpoint(); + if (symbol instanceof DomainSymbol d && d.generic() + && !(effectiveDescriptor.apply(d.typeId()) instanceof CoordinateType)) + diagnostics.emitRule(index.byId(d.id()).orElseThrow(), "GENERIC requires a coordinate domain", "GENERIC_TYPE"); + if (!(symbol instanceof guru.interlis.model.ContextSymbol context)) continue; + DeclInfo decl = index.byId(context.id()).orElseThrow(); + for (var mapping : context.mappings()) { + var generic = (DomainSymbol) state.symbol(mapping.genericDomain()); + if (!generic.generic() || !(effectiveDescriptor.apply(generic.typeId()) instanceof CoordinateType base)) { + diagnostics.emitRule(decl, "CONTEXT domain must be GENERIC coordinates", "CONTEXT_GENERIC"); continue; + } + for (SymbolId id : mapping.alternatives()) { + work.checkpoint(); + var concrete = (DomainSymbol) state.symbol(id); + if (concrete.generic() || concrete.header().modifiers().contains(guru.interlis.model.Modifier.ABSTRACT) + || !(effectiveDescriptor.apply(concrete.typeId()) instanceof CoordinateType target) + || target.axes().stream().anyMatch(a -> a.minimum().isEmpty() || a.maximum().isEmpty())) { + diagnostics.emitRule(decl, "CONTEXT alternative must be concrete coordinates", "CONTEXT_CONCRETE"); continue; + } + for (SymbolId parent : inheritance.baseChain(concrete.id()).stream().skip(1).toList()) { + if (state.symbol(parent) instanceof DomainSymbol ancestor + && effectiveDescriptor.apply(ancestor.typeId()) instanceof CoordinateType inherited + && !guru.interlis.model.internal.TypeCompatibility.coordinateRefines(target, inherited)) + diagnostics.emitRule(decl, "CONTEXT alternative widens its declared coordinate base", "CONTEXT_ALTERNATIVE_EXTENSION"); + } + if (base.dimensions() != target.dimensions() || base.multiCoordinate() != target.multiCoordinate()) + diagnostics.emitRule(decl, "CONTEXT target dimensions differ", "GENERIC_DIMENSIONS"); + else if (!guru.interlis.model.internal.TypeCompatibility.coordinateRefines(target, base)) + diagnostics.emitRule(decl, "CONTEXT target must refine the generic coordinates", "GENERIC_RANGE"); + } + } + } + Map> contexts = new LinkedHashMap<>(); + for (var model : index.models()) modelContexts(model.id(), contexts, new HashSet<>()); + for (Symbol symbol : List.copyOf(state.symbolsInBuildOrder())) { + work.checkpoint(); + if (!(symbol instanceof TopicSymbol topic) || index.byId(topic.id()).isEmpty()) continue; + DeclInfo decl = index.byId(topic.id()).orElseThrow(); + SymbolId model = index.modelOfSymbol(topic.id()).orElseThrow(); + Set visible = importedModels(model); + Set used = new LinkedHashSet<>(); + Set viewables = new LinkedHashSet<>(); + for (SymbolId id : inheritance.baseChain(topic.id())) for (DeclInfo child : index.children(id)) + if (Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION, SymbolKind.VIEW).contains(child.kind())) viewables.add(child.id()); + // EXTENDED replaces the inherited declaration in this topic's effective population. + var overridden = new HashSet(); + for (SymbolId id : viewables) if (state.symbol(id).header().modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) + overridden.addAll(inheritance.baseChain(id).stream().filter(base -> !base.equals(id)).toList()); + viewables.removeAll(overridden); + Set seen = new HashSet<>(); + Map> referenceUses=new LinkedHashMap<>(); + for (SymbolId id : viewables) collectViewableGenerics(id, visible, topic.id(), used, referenceUses, seen); + var available = contexts.getOrDefault(model, Map.of()); + Set declaredDeferred = new LinkedHashSet<>(); + for (SymbolId id : inheritance.baseChain(topic.id())) if (state.symbol(id) instanceof TopicSymbol t) declaredDeferred.addAll(t.deferredGenerics()); + for (SymbolId id : topic.deferredGenerics()) { + if (!(state.symbol(id) instanceof DomainSymbol domain) || !domain.generic()) + diagnostics.emitRule(decl, "DEFERRED GENERICS references a non-generic domain", "GENERIC_DEFERRED_UNKNOWN"); + if (!used.contains(id)) diagnostics.emitRule(decl, "DEFERRED GENERICS domain is unused", "GENERIC_DEFERRED_UNUSED"); + if (!available.containsKey(id)) diagnostics.emitRule(decl, "DEFERRED GENERICS domain has no CONTEXT", "GENERIC_CONTEXT_MISSING"); + } + List bindings = new ArrayList<>(); + for (SymbolId generic : used) { + var binding = available.get(generic); + if(binding!=null) for(var range:referenceUses.getOrDefault(generic,Set.of())) for(var alternative:binding.alternatives()) { + work.checkpoint();var reference=range.referenceSystem().orElseThrow(); + var concrete=new guru.interlis.model.NumericRange(range.minimum(),range.maximum(),range.circular(),range.direction(),range.unit(), + Optional.of(new guru.interlis.model.NumericRange.ReferenceSystem(alternative,true,reference.axis()))); + // Validate known alternatives without rewriting the published generic identity. + metadata.get().validateRange(decl,concrete); + } + if (binding == null) { + if (topic.header().modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) + bindings.add(new TopicSymbol.GenericBinding(generic, List.of(), List.of(), TopicSymbol.BindingState.ABSTRACT)); + else diagnostics.emitRule(decl, "generic domain is used without an effective CONTEXT", "GENERIC_CONTEXT_MISSING"); + } else if (binding.alternatives().size() > 1 && !declaredDeferred.contains(generic)) { + diagnostics.emitRule(decl, "multiple CONTEXT alternatives require DEFERRED GENERICS", "GENERIC_DEFERRED_REQUIRED"); + } else if (!binding.alternatives().isEmpty()) bindings.add(binding); + } + replaceContextTopic.accept(new TopicSymbol(topic.header(), topic.baseTopic(), topic.members(), topic.dependencies(), + topic.deferredGenerics(), bindings)); + } + } + + private Map modelContexts(SymbolId model, + Map> completed, Set active) { + if (state.symbol(model) instanceof ModelSymbol predefined && predefined.origin() instanceof Origin.PredefinedOrigin) return Map.of(); + if (completed.containsKey(model)) return completed.get(model); + if (!active.add(model)) return Map.of(); // ImportBinder diagnoses cycles. + Map result = new LinkedHashMap<>(); + ModelSymbol owner = (ModelSymbol) state.symbol(model); + DeclInfo decl = index.byId(model).orElseThrow(); + for (var imported : owner.imports()) if (imported.targetModel().isPresent()) { + work.checkpoint(); + for (var entry : modelContexts(imported.targetModel().get(), completed, active).entrySet()) { + var previous = result.get(entry.getKey()); var incoming = entry.getValue(); + if (previous == null) result.put(entry.getKey(), incoming); + else { + var candidates = new LinkedHashSet<>(previous.alternatives()); candidates.addAll(incoming.alternatives()); + candidates.removeIf(id -> previous.alternatives().stream().noneMatch(base -> inheritance.domainExtends(id, base)) + || incoming.alternatives().stream().noneMatch(base -> inheritance.domainExtends(id, base))); + var origins = new LinkedHashSet<>(previous.contexts()); origins.addAll(incoming.contexts()); + if (candidates.isEmpty()) diagnostics.emitRule(decl, "imported contexts have incompatible alternatives", "CONTEXT_CONFLICT"); + result.put(entry.getKey(), genericBinding(entry.getKey(), List.copyOf(candidates), List.copyOf(origins))); + } + } + } + for (SymbolId member : owner.members()) if (state.symbol(member) instanceof guru.interlis.model.ContextSymbol context) { + for (var mapping : context.mappings()) { + work.checkpoint(); + var previous = result.get(mapping.genericDomain()); + if (previous != null && mapping.alternatives().stream().anyMatch(id -> + previous.alternatives().stream().noneMatch(base -> inheritance.domainExtends(id, base)))) + diagnostics.emitRule(index.byId(context.id()).orElseThrow(), "CONTEXT target must extend an existing alternative", "GENERIC_CONTEXT_EXTENDS"); + result.put(mapping.genericDomain(), genericBinding(mapping.genericDomain(), mapping.alternatives(), List.of(context.id()))); + } + } + active.remove(model); completed.put(model, result); return result; + } + + private TopicSymbol.GenericBinding genericBinding(SymbolId generic, List alternatives, List contexts) { + // Empty alternatives can occur only in a diagnosed partial analysis, not in a valid schema. + return new TopicSymbol.GenericBinding(generic, alternatives, contexts, alternatives.isEmpty() + ? TopicSymbol.BindingState.ABSTRACT : alternatives.size() == 1 ? TopicSymbol.BindingState.CONCRETE : TopicSymbol.BindingState.DEFERRED); + } + + Set importedModels(SymbolId model) { + Set result = new LinkedHashSet<>(); var pending = new java.util.ArrayDeque(); pending.add(model); + while (!pending.isEmpty()) { + work.checkpoint(); SymbolId next = pending.removeFirst(); + if (result.add(next) && state.symbol(next) instanceof ModelSymbol m) + m.imports().forEach(i -> i.targetModel().ifPresent(pending::add)); + } + return result; + } + + private void collectViewableGenerics(SymbolId viewable, Set visible, SymbolId topic, + Set used, Map> referenceUses, Set seen) { + for (var member : state.effectiveMembers(viewable)) { + work.checkpoint(); + if (state.symbol(member.declaredSymbol()) instanceof AttributeSymbol attr) collectGenericUses(attr.declaredType(), visible, topic, used, referenceUses, seen); + } + } + + private void collectGenericUses(TypeId type, Set visible, SymbolId topic, Set used, + Map> referenceUses, Set seen) { + if (type == null || !seen.add(type)) return; + work.checkpoint(); TypeDescriptor descriptor = state.type(type); + if (descriptor instanceof DomainReferenceType ref && state.symbol(ref.targetDomain()) instanceof DomainSymbol domain) { + if (domain.generic()) used.add(domain.id()); + collectGenericUses(domain.typeId(), visible, topic, used, referenceUses, seen); + } else if (descriptor instanceof GeometryType geometry) { + geometry.vertexDomain().map(state::symbol).filter(DomainSymbol.class::isInstance).map(DomainSymbol.class::cast).ifPresent(domain -> { + if (domain.generic()) used.add(domain.id()); collectGenericUses(domain.typeId(), visible, topic, used, referenceUses, seen); + }); + } else if (descriptor instanceof NumericType numeric) { + collectRangeGenerics(numeric.range(),visible,topic,used,referenceUses,seen); + } else if (descriptor instanceof CoordinateType coordinate) { + for(var range:coordinate.axes())collectRangeGenerics(range,visible,topic,used,referenceUses,seen); + } else if (descriptor instanceof CollectionType collection) { + collectGenericUses(collection.elementType(), visible, topic, used, referenceUses, seen); + } else if (descriptor instanceof StructureValueType structure) { + for (Symbol symbol : state.symbolsInBuildOrder()) if (symbol instanceof StructureSymbol candidate + && visible.contains(index.modelOfSymbol(candidate.id()).orElse(null)) + && (structure.targetRoots().isEmpty() || structure.targetRoots().stream().anyMatch(root -> inheritance.baseChain(candidate.id()).contains(root))) + && (!(state.symbol(scopeRoot.apply(candidate.id())) instanceof TopicSymbol) || inheritance.baseChain(topic).contains(scopeRoot.apply(candidate.id())))) + collectViewableGenerics(candidate.id(), visible, topic, used, referenceUses, seen); + } + } + + private void collectRangeGenerics(guru.interlis.model.NumericRange range,Set visible,SymbolId topic, + Set used,Map> referenceUses,Set seen) { + work.checkpoint();var reference=range.referenceSystem().orElse(null); + if(reference==null||!reference.domain())return; + if(state.symbol(reference.target()) instanceof DomainSymbol domain&&domain.generic()) { + used.add(domain.id());referenceUses.computeIfAbsent(domain.id(),ignored->new LinkedHashSet<>()).add(range); + collectGenericUses(domain.typeId(),visible,topic,used,referenceUses,seen); + } + } + + private String lastSegment(String name) { + int dot = name.lastIndexOf('.'); + return dot < 0 ? name : name.substring(dot + 1); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GraphBuilder.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GraphBuilder.java index ab06ace4..52bb5e27 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GraphBuilder.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GraphBuilder.java @@ -1,5783 +1,236 @@ package guru.interlis.compiler.internal; -import guru.interlis.compiler.ImportResolution; +import guru.interlis.compiler.AnalysisBudget; import guru.interlis.compiler.SourceSet; -import guru.interlis.diagnostics.DiagnosticArgument; -import guru.interlis.diagnostics.DiagnosticCode; -import guru.interlis.diagnostics.DiagnosticDraft; -import guru.interlis.diagnostics.Phase; import guru.interlis.diagnostics.DiagnosticId; -import guru.interlis.diagnostics.Phase; -import guru.interlis.diagnostics.Severity; -import guru.interlis.model.AnalysisCoverage; -import guru.interlis.model.AssociationSymbol; -import guru.interlis.model.AttributeSymbol; -import guru.interlis.model.BindingResolution; -import guru.interlis.model.Cardinality; -import guru.interlis.model.CardinalityUpper; -import guru.interlis.model.ClassSymbol; -import guru.interlis.model.ClassValueType; -import guru.interlis.model.CoordinateType; -import guru.interlis.model.FormattedType; -import guru.interlis.model.internal.FormattedValues; -import guru.interlis.model.GenericType; -import guru.interlis.model.GeometryType; -import guru.interlis.model.StructureValueType; -import guru.interlis.model.CollectionType; -import guru.interlis.model.ConstraintDefinition; -import guru.interlis.model.ConstraintSymbol; -import guru.interlis.model.CoverageStatus; -import guru.interlis.model.DecimalLiteral; -import guru.interlis.model.DeclaredName; -import guru.interlis.model.Documentation; -import guru.interlis.model.DomainConstraint; -import guru.interlis.model.DomainReferenceType; -import guru.interlis.model.DomainSymbol; -import guru.interlis.model.EffectiveMember; -import guru.interlis.model.EffectiveMemberOrigin; -import guru.interlis.model.EffectiveType; -import guru.interlis.model.EnumElementSymbol; -import guru.interlis.model.EnumerationType; -import guru.interlis.model.EnumerationValue; -import guru.interlis.model.ExpressionId; -import guru.interlis.model.ErrorExpression; -import guru.interlis.model.ErrorType; -import guru.interlis.model.MandatoryConstraint; -import guru.interlis.model.MetaAttributeSet; -import guru.interlis.model.ModelImport; +import guru.interlis.execution.CancellationToken; import guru.interlis.model.ModelSymbol; -import guru.interlis.model.NameReference; -import guru.interlis.model.NumericRange; -import guru.interlis.model.NumericType; -import guru.interlis.model.Origin; -import guru.interlis.model.PendingReason; -import guru.interlis.model.PendingType; -import guru.interlis.model.PlausibilityConstraint; -import guru.interlis.model.ReferenceId; -import guru.interlis.model.ReferenceType; -import guru.interlis.model.ReferenceSegment; -import guru.interlis.model.ReferenceUsage; -import guru.interlis.model.RoleAggregation; -import guru.interlis.model.RoleSymbol; -import guru.interlis.model.ScopeId; import guru.interlis.model.SemanticGraph; -import guru.interlis.model.StructureSymbol; +import guru.interlis.model.SnapshotId; import guru.interlis.model.Symbol; import guru.interlis.model.SymbolHeader; import guru.interlis.model.SymbolId; -import guru.interlis.model.SymbolKind; -import guru.interlis.model.TextKind; -import guru.interlis.model.TextType; -import guru.interlis.model.TopicSymbol; import guru.interlis.model.TypeDescriptor; import guru.interlis.model.TypeId; -import guru.interlis.model.TypedExpression; -import guru.interlis.model.UnitSymbol; -import guru.interlis.model.FunctionSymbol; -import guru.interlis.model.AlignmentType; -import guru.interlis.model.BlackboxType; -import guru.interlis.model.BooleanType; -import guru.interlis.model.FunctionParameter; -import guru.interlis.model.UniquenessConstraint; -import guru.interlis.model.UnsupportedSymbol; -import guru.interlis.model.ViewSymbol; import guru.interlis.source.SourceId; -import guru.interlis.source.SourceRange; -import guru.interlis.source.SourceRevision; import guru.interlis.source.SourceText; -import guru.interlis.syntax.AssociationDeclarationSyntax; -import guru.interlis.syntax.AttributeDeclarationSyntax; -import guru.interlis.syntax.BinaryExpressionSyntax; -import guru.interlis.syntax.BooleanTypeSyntax; -import guru.interlis.syntax.ClassDeclarationSyntax; -import guru.interlis.syntax.AlignmentTypeSyntax; -import guru.interlis.syntax.BlackboxTypeSyntax; -import guru.interlis.syntax.ClassValueTypeSyntax; -import guru.interlis.syntax.CollectionTypeSyntax; -import guru.interlis.syntax.OidTypeSyntax; -import guru.interlis.syntax.ConstantExpressionSyntax; -import guru.interlis.syntax.ConstraintDeclarationSyntax; -import guru.interlis.syntax.CoordinateTypeSyntax; -import guru.interlis.syntax.DefinedExpressionSyntax; -import guru.interlis.syntax.DomainDeclarationSyntax; -import guru.interlis.syntax.ExternalConstraintSyntax; -import guru.interlis.syntax.FormattedTypeSyntax; -import guru.interlis.syntax.BuiltinFunctionExpressionSyntax; -import guru.interlis.syntax.ClassConstantExpressionSyntax; -import guru.interlis.syntax.GenericConstantExpressionSyntax; -import guru.interlis.syntax.GenericTypeSyntax; -import guru.interlis.syntax.EnumTreeValueSyntax; -import guru.interlis.syntax.EnumerationConstantExpressionSyntax; -import guru.interlis.syntax.FunctionCallExpressionSyntax; -import guru.interlis.syntax.LineTypeSyntax; -import guru.interlis.syntax.ViewDeclarationSyntax; -import guru.interlis.syntax.UnitDeclarationSyntax; -import guru.interlis.syntax.NumericTypeSyntax; -import guru.interlis.syntax.FunctionDeclarationSyntax; -import guru.interlis.syntax.DomainReferenceSyntax; -import guru.interlis.syntax.EnumerationTypeSyntax; -import guru.interlis.syntax.ExpressionSyntax; -import guru.interlis.syntax.LiteralExpressionSyntax; -import guru.interlis.syntax.ParenthesizedExpressionSyntax; -import guru.interlis.syntax.PathExpressionSyntax; -import guru.interlis.syntax.ReferenceTypeSyntax; -import guru.interlis.syntax.RoleDeclarationSyntax; -import guru.interlis.syntax.StructureDeclarationSyntax; -import guru.interlis.syntax.TemporalTypeSyntax; -import guru.interlis.syntax.TextTypeSyntax; -import guru.interlis.syntax.TopicSyntax; -import guru.interlis.syntax.TypeSyntax; -import guru.interlis.syntax.UnaryExpressionSyntax; -import guru.interlis.syntax.UndefinedExpressionSyntax; -import guru.interlis.syntax.UnsupportedExpressionSyntax; -import java.math.BigInteger; import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Function; -/** Builds the immutable semantic graph for the parsed declarations. */ +/** Coordinates the ordered construction and publication of one semantic graph. */ final class GraphBuilder { - final IndexedDeclarations index; + private final IndexedDeclarations index; private final SourceSet sourceSet; private final Closure closure; - private final guru.interlis.model.SnapshotId snapshotId; - final NameResolver resolver; - private final DiagnosticCollector collector; - private final Map> syntaxUnsupportedBySource; - final Map symbols = new LinkedHashMap<>(); - final Map types = new LinkedHashMap<>(); - final Map expressions = new LinkedHashMap<>(); - private final Map references = new LinkedHashMap<>(); - private final Set recordedReferences = new HashSet<>(); - final Map declaredTypes = new LinkedHashMap<>(); - private final Map effectiveTypes = new LinkedHashMap<>(); - final Map> effectiveMembers = new LinkedHashMap<>(); - private final Map externalConstraintTargets = new LinkedHashMap<>(); - private final Set resolvedObjectBases = new HashSet<>(); - private final Set resolvingDomainTypes = new HashSet<>(); - private final Set resolvingAttributeTypes = new HashSet<>(); - private final Set buildingAssociations = new HashSet<>(); - final Map baseOf = new LinkedHashMap<>(); - private final Map modelIdByDecl = new LinkedHashMap<>(); - private final Map> unsupportedBySource = new LinkedHashMap<>(); - private final Map modelNames = new LinkedHashMap<>(); - private int typeCounter; - private int expressionCounter; - private int referenceCounter; - private final guru.interlis.execution.CancellationToken cancellation; - private final List syntaxErrors; - private final long maximumWork; - private long work; + private final SnapshotId snapshotId; + private final SemanticBuildState state = new SemanticBuildState(); + private final SemanticWork work; + private final SemanticDiagnostics diagnostics; private final OverlayBindings overlays; - private final Map booleanTypes = new java.util.EnumMap<>(guru.interlis.model.LanguageVersion.class); + private final Map> syntaxUnsupportedBySource; + private final DeclarationBindings declarations; + private final InheritanceBindings inheritance; + private final ConstraintBindings constraints; + private final TopicBindings topics; + private final ExpressionBindings expressions; + private final ObjectPathBindings paths; + private final TypeBindings types; + private final UnitBindings units; + private final OidBindings oid; + private final FormattedTypeBindings formats; + private final GenericBindings generics; + private final LineForms lines; + private final GraphicBuilder graphics; + private final MetadataBindings metadata; + private final StructureBindings structures; + private final PredefinedTemporal temporal; + private final ViewBuilder views; + private final ReferenceBindings references; + private final TranslationBindings translations; GraphBuilder(IndexedDeclarations index, SourceSet sourceSet, Closure closure, - DiagnosticCollector collector, guru.interlis.model.SnapshotId snapshotId, + DiagnosticCollector collector, SnapshotId snapshotId, Map> syntaxUnsupportedBySource, - guru.interlis.execution.CancellationToken cancellation, - guru.interlis.compiler.AnalysisBudget budget, OverlayBindings overlays) { - this.overlays=overlays;this.work=overlays.workUnits(); - this.cancellation = Objects.requireNonNull(cancellation, "cancellation"); - this.maximumWork = Objects.requireNonNull(budget, "budget").maxSemanticWorkUnits(); + CancellationToken cancellation, AnalysisBudget budget, OverlayBindings overlays) { + this.overlays = overlays; + this.work = new SemanticWork(cancellation, + Objects.requireNonNull(budget, "budget").maxSemanticWorkUnits(), overlays.workUnits()); this.index = Objects.requireNonNull(index, "index"); this.sourceSet = Objects.requireNonNull(sourceSet, "sourceSet"); this.closure = Objects.requireNonNull(closure, "closure"); - this.collector = Objects.requireNonNull(collector, "collector"); - this.syntaxErrors = collector.snapshot().stream().filter(d -> d.severity() == Severity.ERROR - && (d.phase() == Phase.LEXING || d.phase() == Phase.PARSING)).toList(); + this.diagnostics = new SemanticDiagnostics(collector, this.closure); this.snapshotId = Objects.requireNonNull(snapshotId, "snapshotId"); - this.syntaxUnsupportedBySource = Map.copyOf( - Objects.requireNonNull(syntaxUnsupportedBySource, "syntaxUnsupportedBySource")); - this.resolver = new NameResolver(index, closure); - for (IndexedDeclarations.ModelEntry entry : index.models()) { - checkWork(); - modelIdByDecl.put(entry.declaration().id(), entry.id()); - } - } - - void checkWork() { - cancellation.throwIfCancellationRequested(); - if (++work > maximumWork) { - throw new AnalysisBudgetExceededException( - "maxSemanticWorkUnits", maximumWork); - } - } - - final LineForms lines = new LineForms(this); - final GraphicBuilder graphics = new GraphicBuilder(this); - final MetadataBindings metadata = new MetadataBindings(this); - final StructureBindings structures = new StructureBindings(this); - final FunctionBindings functions = new FunctionBindings(this); - final PredefinedTemporal temporal = new PredefinedTemporal(this); + this.syntaxUnsupportedBySource = Map.copyOf(Objects.requireNonNull(syntaxUnsupportedBySource, "syntaxUnsupportedBySource")); + var resolver = new NameResolver(index, closure); + this.references = new ReferenceBindings(index, state, work, state::storeReference, + language -> PredefinedModels.ensure(state, language)); + var names = new NameBindings(index, resolver, work, diagnostics, references, + (owner, name, kinds) -> temporal().find(owner, name, kinds), + (owner, name, kinds) -> metadata().predefined(owner, name, kinds), + (owner, name, kinds) -> lines().predefined(owner, name, kinds), + (owner, name) -> oid().builtinOidDomain(owner, name), + (owner, name) -> types().builtinSimpleDomain(owner, name), + (owner, name) -> units().builtinUnit(owner, name), + (signature, language) -> expressions().builtinDeclaration(signature, language)); + Function ensureSymbol = declaration -> declarations().buildSymbol(declaration); + Function symbolHeader = declaration -> declarations().header(declaration); + Function effectiveDescriptor = id -> types().effectiveDescriptor(id); + Function scopeRoot = id -> paths().scopeRoot(id); + + this.units = new UnitBindings(state, work, index, diagnostics, names, this::temporal, + state::defineSymbol, symbolHeader, ensureSymbol, effectiveDescriptor); + this.oid = new OidBindings(state, work, index, diagnostics, names, this::types, this::inheritance, + state::defineSymbol, state::replaceSymbol, scopeRoot); + this.topics = new TopicBindings(state, work, index, diagnostics, this::inheritance, this::paths, oid, + effectiveDescriptor); + this.inheritance = new InheritanceBindings(state, work, index, diagnostics, names, this::types, topics, + this::metadata, state::replaceSymbol, scopeRoot, ensureSymbol); + this.generics = new GenericBindings(state, work, index, diagnostics, names, inheritance, this::metadata, + state::replaceSymbol, symbolHeader, effectiveDescriptor, scopeRoot); + this.structures = new StructureBindings(state, work, index, diagnostics, names, inheritance, + this::paths, generics, effectiveDescriptor); + this.formats = new FormattedTypeBindings(state, work, index, diagnostics, names, this::types, + inheritance, this::paths, references, structures); + this.lines = new LineForms(state, work, index, diagnostics, names, inheritance, references, this::types, + units, this::expressions, symbolHeader, ensureSymbol); + this.constraints = new ConstraintBindings(state, work, index, diagnostics, resolver, this::paths, + inheritance, references, this::expressions, this::types, structures, symbolHeader); + this.paths = new ObjectPathBindings(state, work, index, diagnostics, resolver, sourceSet, cancellation, + this::expressions, this::views, this::types, inheritance, references, structures, + owner -> graphics().context(owner), ensureSymbol); + this.metadata = new MetadataBindings(state, work, index, diagnostics, resolver, names, references, + inheritance, units, this::types, paths, structures, this::expressions, symbolHeader, + ensureSymbol); + this.temporal = new PredefinedTemporal(state, units, this::types, metadata, inheritance, paths); + this.types = new TypeBindings(state, work, index, diagnostics, units, names, inheritance, lines, paths, + this::expressions, temporal, structures, metadata, references, formats, this::declarations, + state::defineSymbol); + this.graphics = new GraphicBuilder(state, work, index, diagnostics, names, metadata, inheritance, + this::expressions, types, references, topics, symbolHeader, ensureSymbol, scopeRoot); + var functions = new FunctionBindings(state, work, index, diagnostics, this::expressions, types, paths, + (left, right) -> translations().sameBoundExpression(left, right)); + this.expressions = new ExpressionBindings(state, work, index, diagnostics, sourceSet, paths, + this::views, types, lines, metadata, names, references, functions, units, formats, + state::defineSymbol, + (constraint, fallback) -> constraints().externalConstraintTarget(constraint, fallback), + owner -> graphics().context(owner), ensureSymbol); + this.views = new ViewBuilder(state, work, index, diagnostics, resolver, references, inheritance, + expressions, types, paths, structures, topics, lines, ensureSymbol, symbolHeader); + this.declarations = new DeclarationBindings(state, work, index, diagnostics, resolver, closure, + overlays, units, lines, metadata, generics, constraints, graphics, views, names, inheritance, + references, types, expressions); + this.translations = new TranslationBindings(index, state, work, diagnostics, types::effectiveDescriptor, + paths::findMember, views, state::replaceSymbol); + for (var _ : index.models()) { + work.checkpoint(); + } + } + + // Recursive collaborators are invoked only after construction. + private ConstraintBindings constraints() { return constraints; } + private DeclarationBindings declarations() { return declarations; } + private ExpressionBindings expressions() { return expressions; } + private GraphicBuilder graphics() { return graphics; } + private InheritanceBindings inheritance() { return inheritance; } + private LineForms lines() { return lines; } + private MetadataBindings metadata() { return metadata; } + private OidBindings oid() { return oid; } + private ObjectPathBindings paths() { return paths; } + private PredefinedTemporal temporal() { return temporal; } + private TranslationBindings translations() { return translations; } + private TypeBindings types() { return types; } + private UnitBindings units() { return units; } + private ViewBuilder views() { return views; } GraphResult build() { syntaxUnsupportedBySource.forEach((source, features) -> { - Set known = unsupportedBySource.computeIfAbsent(source, - ignored -> new LinkedHashSet<>()); for (String feature : features) { - checkWork(); - if (known.add(feature)) { - SourceText text = sourceSet.sources().get(source); - SourceRange range = text == null ? null : text.range(0, 0); - emitUnsupported(source, range, feature, feature); - } + work.checkpoint(); + SourceText text = sourceSet.sources().get(source); + diagnostics.emitUnsupported(source, text == null ? null : text.range(0, 0), feature, feature); } }); for (IndexedDeclarations.ModelEntry entry : index.models()) { - checkWork(); + work.checkpoint(); for (DeclInfo child : index.children(entry.id())) { - checkWork(); - buildSymbol(child); + work.checkpoint(); + declarations.buildSymbol(child); } } for (IndexedDeclarations.ModelEntry entry : index.models()) { - checkWork(); - buildModel(entry); + work.checkpoint(); + declarations.buildModel(entry); } - validateTopicInheritanceCycles(); - validateUnits(); - resolveOverridesAndEffectiveMembers(); + inheritance.validateTopicInheritanceCycles(); + units.validateUnits(); + inheritance.resolveOverridesAndEffectiveMembers(); structures.validate(); lines.validateConcreteMembers(); metadata.validate(); - validateGenericSystem(); - bindOidRules(); - validateTopicDependencies(); + generics.validateGenericSystem(); + oid.bindOidRules(); + topics.validateTopicDependencies(); views.validateDependencies(); graphics.validateDependencies(); - validateReferenceTargets(); - buildEffectiveTypes(); - validateFormattedUses(); - validateNumericUses(); - new NumericSubdivisions(this).validate(); - buildReferences(); - validateAssociationRules(); - validateAttributeOverrides(); - validateClassExtensions(); - validateTypeNamespaces(); - validateConstraintNames(); - overlays.validateCrs(this); - validateTranslations(); - bindTransferNamespaces(); + topics.validateReferenceTargets(); + types.buildEffectiveTypes(); + formats.validateFormattedUses(); + units.validateNumericUses(); + new NumericSubdivisions(state, work, index, diagnostics, types::effectiveDescriptor).validate(); + references.complete(); + inheritance.validateAssociationRules(); + inheritance.validateAttributeOverrides(); + inheritance.validateClassExtensions(); + declarations.validateTypeNamespaces(); + constraints.validateConstraintNames(); + overlays.validateCrs(state, work, types); + translations.complete(); + declarations.bindTransferNamespaces(); Set modelIds = closure.models().stream() - .map(m -> findModelId(m.name(), m.declarationOffset(), m.sourceId()).orElse(null)) + .map(m -> index.findModelId(m.name(), m.declarationOffset(), m.sourceId()).orElse(null)) .filter(Objects::nonNull) .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); List rootIds = closure.roots().stream() - .map(m -> findModelId(m.name(), m.declarationOffset(), m.sourceId()).orElse(null)) + .map(m -> index.findModelId(m.name(), m.declarationOffset(), m.sourceId()).orElse(null)) .filter(Objects::nonNull) .toList(); Set languages = new LinkedHashSet<>(); - for (ModelSymbol model : symbols.values().stream() + for (ModelSymbol model : state.symbolsInBuildOrder().stream() .filter(s -> s instanceof ModelSymbol) .map(s -> (ModelSymbol) s) .filter(s -> modelIds.contains(s.id())) .toList()) { - checkWork(); + work.checkpoint(); languages.add(model.header().languageVersion()); } - PredefinedModels.finish(this, languages); - SemanticGraph graph = new SemanticGraph( - snapshotId, - symbols.values(), types.values(), expressions.values().stream().toList(), - references.values(), declaredTypes, effectiveTypes, effectiveMembers, - rootIds, new ArrayList<>(modelIds), languages, + PredefinedModels.finish(work, state, languages); + SemanticGraph graph = state.freeze( + snapshotId, rootIds, new ArrayList<>(modelIds), languages, sourceSet.sources().entrySet().stream().collect(java.util.stream.Collectors.toMap( Map.Entry::getKey, e -> e.getValue().revision()))); - AnalysisCoverage coverage = buildCoverage(); - return new GraphResult(graph, coverage, rootIds, new ArrayList<>(modelIds), - unsupportedBySource); - } - - private void validateTypeNamespaces() { - var typeKinds = DeclarationNamespaces.TYPE_KINDS; - var names = new LinkedHashMap>(); - var declarations = index.all().stream().sorted(Comparator.comparingInt(DeclInfo::order)).toList(); - for (var declaration : declarations) { - checkWork(); - if (declaration.owner().isPresent() && declaration.nameRange().isPresent() - && typeKinds.contains(declaration.kind())) { - var scope = names.computeIfAbsent(declaration.owner().orElseThrow(), ignored -> new LinkedHashMap<>()); - var prior = scope.putIfAbsent(declaration.name(), declaration); - if (prior != null) emitNamespaceRule(declaration, prior, - "type names must be unique in their declaring scope", "SCOPE_TYPE_NAME_UNIQUE"); - } - } - for (var declaration : declarations) { - checkWork(); - if (declaration.owner().isEmpty() || declaration.nameRange().isEmpty() - || !typeKinds.contains(declaration.kind())) continue; - var owner = declaration.owner().orElseThrow(); - var outer = index.byId(owner).flatMap(DeclInfo::owner); - while (outer.isPresent()) { - checkWork(); - var prior = names.getOrDefault(outer.orElseThrow(), Map.of()).get(declaration.name()); - if (prior != null) { - emitNamespaceRule(declaration, prior, - "local type names must not shadow an enclosing type name", "SCOPE_OUTER_TYPE_NAME"); - break; - } - outer = index.byId(outer.orElseThrow()).flatMap(DeclInfo::owner); - } - if (declaration.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) continue; - var seen = new HashSet(); - for (var inherited = baseOf.get(owner); inherited != null && seen.add(inherited); inherited = baseOf.get(inherited)) { - checkWork(); - var prior = names.getOrDefault(inherited, Map.of()).get(declaration.name()); - if (prior != null) { - emitNamespaceRule(declaration, prior, - "inherited type names require an explicit EXTENDED declaration", "SCOPE_INHERITED_TYPE_NAME"); - break; - } - } - } - } - - private void emitNamespaceRule(DeclInfo declaration, DeclInfo prior, String rule, String reason) { - var related = new guru.interlis.diagnostics.RelatedLocation(prior.nameRange().orElseThrow(), - "diagnostic.related.conflictingDeclaration", - List.of(new DiagnosticArgument.IdentifierArgument("name", prior.name()))); - emitRule(declaration, rule, reason, ruleRange(declaration), List.of(related)); - } - - /** Named constraints share the declaring viewable's local namespace, including - * same-topic CONSTRAINTS OF blocks. Inherited-topic contexts remain distinct. */ - private void validateConstraintNames() { - record ConstraintScope(SymbolId target, SymbolId topic) {} - Map> names = new LinkedHashMap<>(); - var declarations = index.all().stream() - .filter(d -> d.kind() == SymbolKind.CONSTRAINT && d.language() == guru.interlis.model.LanguageVersion.ILI24) - .sorted(Comparator.comparing((DeclInfo d) -> d.sourceId().value()) - .thenComparingInt(d -> d.syntax().range().startOffset())).toList(); - for (var declaration : declarations) { - checkWork(); - var syntax = declaration.syntax() instanceof ExternalConstraintSyntax external - ? external.constraint() : (ConstraintDeclarationSyntax) declaration.syntax(); - if (syntax.name().isEmpty()) continue; - SymbolId target = externalConstraintTargets.getOrDefault(declaration.id(), declaration.owner().orElse(null)); - var symbol = symbols.get(target); - if (!(symbol instanceof ClassSymbol || symbol instanceof StructureSymbol - || symbol instanceof AssociationSymbol || symbol instanceof ViewSymbol)) continue; - var scope = new ConstraintScope(target, scopeRoot(declaration.id())); - if (!names.computeIfAbsent(scope, ignored -> new LinkedHashSet<>()).add(syntax.name().orElseThrow())) - emitRule(declaration, "constraint names must be unique within their declaring class", - "CONSTRAINT_NAME", declaration.nameRange().orElse(syntax.range())); - } - } - - private void validateTranslations() { - List models = symbols.values().stream() - .filter(ModelSymbol.class::isInstance).map(ModelSymbol.class::cast).toList(); - Map originals = new LinkedHashMap<>(); - for (var model : models) model.translation().ifPresent(translation -> { - var candidates = models.stream().filter(candidate -> candidate.name().value().equals(translation.sourceModel())) - .filter(candidate -> translation.sourceVersion().isEmpty() || translation.sourceVersion().orElseThrow().equals(candidate.modelVersion())).toList(); - if(translation.sourceTarget().isPresent()) originals.put(model.id(),translation.sourceTarget().orElseThrow()); - else if (candidates.size() == 1) originals.put(model.id(), candidates.getFirst().id()); - }); - for (ModelSymbol model : models) { - checkWork(); - if (model.translation().isEmpty()) { - continue; - } - guru.interlis.model.ModelTranslation translation = model.translation().orElseThrow(); - ModelSymbol source = (ModelSymbol) symbols.get(originals.get(model.id())); - DeclInfo decl = index.byId(model.id()).orElse(null); - if (decl != null && translationRoot(model.id(), originals) == null) { - emitRule(decl, "translation dependency has no original model", "TRANSLATION_CYCLE"); - continue; - } - if (source == null) { - if (decl != null) { - emitRule(decl, "translation source model not found", "TRANSLATION_SOURCE"); - } - continue; - } - if (translation.sourceVersion().isPresent() - && !translation.sourceVersion().orElseThrow().equals(source.modelVersion())) { - if (decl != null) { - emitRule(decl, "translation source version mismatch", "TRANSLATION_VERSION"); - } - } - if (decl != null) { - Map correspondence = new LinkedHashMap<>(); - if (!correspondenceWithImports(source, model, originals, correspondence, new HashSet<>())) correspondence.clear(); - if (correspondence.isEmpty() - || !sameModelProfile(source, model)) { - emitRule(decl, "translation syntax does not match source model", - "TRANSLATION_STRUCTURE"); - } - if (!correspondence.isEmpty()) { - var checked = new LinkedHashSet(); - while (true) { - checkWork(); - var pending = correspondence.entrySet().stream().filter(e -> !checked.contains(e.getKey())) - .map(e -> new guru.interlis.model.TranslationLink(e.getKey(), e.getValue())).toList(); - if (pending.isEmpty()) break; - for (var pair : pending) { - checkWork(); - checked.add(pair.original()); - var left=symbols.get(pair.original());var right=symbols.get(pair.translated()); - if(left!=null && right!=null)compareSymbolReferences(left,right,correspondence,decl); - } - } - } - // Expression/formation checks may add verified contextualized declarations. - // Publish the final immutable links only after all newly added pairs were checked. - var bound = new guru.interlis.model.ModelTranslation(translation.sourceModel(), translation.sourceVersion(), translation.language(), - Optional.of(source.id()), correspondence.entrySet().stream().map(e -> new guru.interlis.model.TranslationLink(e.getKey(), e.getValue())).toList(),translation.origin()); - symbols.put(model.id(), new ModelSymbol(model.header(), model.modelKind(), model.contracted(), model.modelVersion(), - model.issuer(), model.imports(), model.members(), Optional.of(bound), model.options(), model.transferNamespace())); - } - } - } - - private boolean sameModelProfile(ModelSymbol source, ModelSymbol target) { - if (source.modelKind() != target.modelKind() - || source.header().languageVersion() == guru.interlis.model.LanguageVersion.ILI23 && source.contracted() != target.contracted() - || source.options().incrementalTransferAllowed() != target.options().incrementalTransferAllowed() - || !source.options().characterSet().map(v -> CharacterSets.canonical(v.value()).orElse(v.value())) - .equals(target.options().characterSet().map(v -> CharacterSets.canonical(v.value()).orElse(v.value()))) - || source.imports().size() != target.imports().size()) { - return false; - } - return true; - } - - Optional baseTopicMember(SymbolId context, String name, - Set expected) { - SymbolId current = scopeRoot(context); - Set seen = new HashSet<>(); - while (current != null && seen.add(current)) { - checkWork(); - DeclInfo topicDecl = index.byId(current).orElse(null); - if (topicDecl == null || !(topicDecl.syntax() instanceof guru.interlis.syntax.TopicSyntax syntax) - || syntax.baseTopic().isEmpty()) { - return Optional.empty(); - } - Optional base = tryResolve(topicDecl, syntax.baseTopic().orElseThrow(), - Set.of(SymbolKind.TOPIC)); - if (base.isEmpty()) { - return Optional.empty(); - } - for (DeclInfo child : index.children(base.orElseThrow())) { - checkWork(); - if (expected.contains(child.kind()) && child.name().equals(name)) { - return Optional.of(child.id()); - } - } - current = base.orElseThrow(); - } - return Optional.empty(); - } - - /** Import pairs follow their bound targets, never the available language variants. */ - private boolean correspondenceWithImports(ModelSymbol source, ModelSymbol target, - Map originals, Map map, - Set> visited) { - checkWork(); - if (!visited.add(List.of(source.id(), target.id()))) return true; - if (!correspondenceInto(source, target, map) || source.imports().size() != target.imports().size()) return false; - Set used = new HashSet<>(); - for (var imported : source.imports()) { - checkWork(); - List exact = new ArrayList<>(), translated = new ArrayList<>(); - for (int i = 0; i < target.imports().size(); i++) { - if (used.contains(i)) continue; - var candidate = target.imports().get(i); - if (candidate.unqualified() != imported.unqualified()) continue; - if (imported.targetModel().isEmpty() || candidate.targetModel().isEmpty()) { - if (imported.importedModel().equals("INTERLIS") && candidate.importedModel().equals("INTERLIS")) exact.add(i); - } else if (imported.targetModel().equals(candidate.targetModel())) exact.add(i); - else { - SymbolId root = translationRoot(imported.targetModel().orElseThrow(), originals); - if (root != null && root.equals(translationRoot(candidate.targetModel().orElseThrow(), originals))) translated.add(i); - } - } - var candidates = exact.isEmpty() ? translated : exact; - if (candidates.size() != 1) return false; - int selected = candidates.getFirst(); used.add(selected); - if (imported.targetModel().isPresent()) { - var a = symbols.get(imported.targetModel().orElseThrow()); - var b = symbols.get(target.imports().get(selected).targetModel().orElseThrow()); - if (!(a instanceof ModelSymbol x) || !(b instanceof ModelSymbol y) - || !correspondenceWithImports(x, y, originals, map, visited)) return false; - } - } - return true; - } - - private SymbolId translationRoot(SymbolId model, Map originals) { - var visited = new LinkedHashSet(); - var current = model; - while (originals.containsKey(current)) { - checkWork(); - if (!visited.add(current)) return null; - current = originals.get(current); - } - return current; - } - - private boolean correspondenceInto(Symbol source, Symbol target, - Map map) { - if (source.kind() != target.kind()) { - return false; - } - SymbolId previous = map.putIfAbsent(source.id(), target.id()); - if (previous != null && !previous.equals(target.id())) return false; - if (source instanceof DomainSymbol || source instanceof AttributeSymbol) { - var a = index.children(source.id()).stream().filter(d -> d.kind() == SymbolKind.ENUM_ELEMENT).map(DeclInfo::id).toList(); - var b = index.children(target.id()).stream().filter(d -> d.kind() == SymbolKind.ENUM_ELEMENT).map(DeclInfo::id).toList(); - if (!childCorrespondence(a, b, map)) return false; - } - var sourceConstraints=index.children(source.id()).stream().filter(d->d.kind()==SymbolKind.CONSTRAINT).map(DeclInfo::id).toList(); - var targetConstraints=index.children(target.id()).stream().filter(d->d.kind()==SymbolKind.CONSTRAINT).map(DeclInfo::id).toList(); - if(!constraintCorrespondence(sourceConstraints,targetConstraints,map))return false; - if (source instanceof ModelSymbol sourceModel && target instanceof ModelSymbol targetModel) { - return childCorrespondence(sourceModel.members(), targetModel.members(), map); - } - if (source instanceof guru.interlis.model.GraphicSymbol a && target instanceof guru.interlis.model.GraphicSymbol b) - return childCorrespondence(a.rules(), b.rules(), map); - if (source instanceof guru.interlis.model.MetadataBasketSymbol a && target instanceof guru.interlis.model.MetadataBasketSymbol b) - return childCorrespondence(a.objects(), b.objects(), map); - if (source instanceof TopicSymbol sourceTopic && target instanceof TopicSymbol targetTopic) { - return childCorrespondence(sourceTopic.members(), targetTopic.members(), map); - } - if (source instanceof ClassSymbol sourceClass && target instanceof ClassSymbol targetClass) { - if (!childCorrespondence(sourceClass.members(), targetClass.members(), map)) { - return false; - } - return constraintCorrespondence(sourceClass.constraints(), targetClass.constraints(), map); - } - if (source instanceof StructureSymbol sourceStructure - && target instanceof StructureSymbol targetStructure) { - if (!childCorrespondence(sourceStructure.members(), targetStructure.members(), map)) { - return false; - } - return constraintCorrespondence(sourceStructure.constraints(), - targetStructure.constraints(), map); - } - if (source instanceof ViewSymbol a && target instanceof ViewSymbol b) { - if (a.formation().orElse(null) instanceof ViewSymbol.Aggregation - && b.formation().orElse(null) instanceof ViewSymbol.Aggregation) { - var left = findMember(a.id(), "AGGREGATES"); - var right = findMember(b.id(), "AGGREGATES"); - if (left.isEmpty() || right.isEmpty()) return false; - var existingAggregate = map.putIfAbsent(left.orElseThrow(), right.orElseThrow()); - if (existingAggregate != null && !existingAggregate.equals(right.orElseThrow())) return false; - } - return childCorrespondence(a.bases(), b.bases(), map) - && childCorrespondence(a.members(), b.members(), map) - && constraintCorrespondence(a.constraints(), b.constraints(), map); - } - if (source instanceof AssociationSymbol sourceAssociation - && target instanceof AssociationSymbol targetAssociation) { - if (sourceAssociation.derivedFrom().isPresent() != targetAssociation.derivedFrom().isPresent()) return false; - if (sourceAssociation.derivedFrom().isPresent()) { - var existingAssociation = map.putIfAbsent(sourceAssociation.derivedFrom().orElseThrow(), targetAssociation.derivedFrom().orElseThrow()); - if (existingAssociation != null && !existingAssociation.equals(targetAssociation.derivedFrom().orElseThrow())) return false; - } - if (!childCorrespondence(sourceAssociation.roles(), targetAssociation.roles(), map) - || !childCorrespondence(sourceAssociation.attributes(), - targetAssociation.attributes(), map)) { - return false; - } - return constraintCorrespondence(sourceAssociation.constraints(), - targetAssociation.constraints(), map); - } - return true; - } - - private boolean childCorrespondence(List sourceIds, List targetIds, - Map map) { - if (sourceIds.size() != targetIds.size()) { - return false; - } - for (int i = 0; i < sourceIds.size(); i++) { - checkWork(); - Symbol source = symbols.get(sourceIds.get(i)); - Symbol target = symbols.get(targetIds.get(i)); - if (source == null || target == null || !correspondenceInto(source, target, map)) { - return false; - } - } - return true; - } - - private boolean constraintCorrespondence(List sourceIds, List targetIds, - Map map) { - if (sourceIds.size() != targetIds.size()) { - return false; - } - for (int i = 0; i < sourceIds.size(); i++) { - checkWork(); - var previous = map.putIfAbsent(sourceIds.get(i), targetIds.get(i)); - if (previous != null && !previous.equals(targetIds.get(i))) return false; - } - return true; - } - - private void compareSymbolReferences(Symbol source, Symbol target, - Map correspondence, - DeclInfo translationDecl) { - if(!source.header().modifiers().equals(target.header().modifiers())) - emitRule(translationDecl,"translation declaration modifiers differ","TRANSLATION_STRUCTURE"); - if(source instanceof AttributeSymbol a && target instanceof AttributeSymbol b - && (a.mandatory()!=b.mandatory() || a.transientAttribute()!=b.transientAttribute() - || a.continuous()!=b.continuous() || a.subdivision()!=b.subdivision())) - emitRule(translationDecl,"translation attribute flags differ","TRANSLATION_REFERENCE"); - if(source instanceof RoleSymbol a && target instanceof RoleSymbol b - && (!a.cardinality().equals(b.cardinality()) || a.aggregation()!=b.aggregation() - || a.ordered()!=b.ordered() || a.external()!=b.external() || a.hiding()!=b.hiding())) - emitRule(translationDecl,"translation role properties differ","TRANSLATION_REFERENCE"); - if(source instanceof AssociationSymbol a && target instanceof AssociationSymbol b - && (!a.cardinality().equals(b.cardinality()) || !sameMapped(a.derivedFrom(),b.derivedFrom(),correspondence))) - emitRule(translationDecl,"translation association properties differ","TRANSLATION_REFERENCE"); - if(source instanceof DomainSymbol a && target instanceof DomainSymbol b && a.generic()!=b.generic()) - emitRule(translationDecl,"translation domain generic flag differs","TRANSLATION_REFERENCE"); - if(source instanceof TopicSymbol a && target instanceof TopicSymbol b - && !new HashSet<>(a.dependencies().stream().map(id->correspondence.getOrDefault(id,id)).toList()).equals(new HashSet<>(b.dependencies()))) - emitRule(translationDecl,"translation topic dependencies differ","TRANSLATION_REFERENCE"); - if(source instanceof ConstraintSymbol a && target instanceof ConstraintSymbol b - && !constraintHasErrors(a.definition()) && !constraintHasErrors(b.definition())) { - boolean same = sameMapped(a.target(),b.target(),correspondence) - && sameConstraint(a.definition(),b.definition(),correspondence); - var targets=constraintReferenceTargets(a.definition()).stream().map(id->correspondence.getOrDefault(id,id)).toList(); - if(!sameMapped(a.target(),b.target(),correspondence) || !targets.equals(constraintReferenceTargets(b.definition()))) - emitRule(translationDecl,"translation constraint targets differ","TRANSLATION_REFERENCE"); - else if(!same) - emitRule(translationDecl,"translation constraint structure differs","TRANSLATION_CONSTRAINT"); - } - var sourceCrs=Optional.ofNullable(source.header().metaAttributes().values().get("CRS")); - var targetCrs=Optional.ofNullable(target.header().metaAttributes().values().get("CRS")); - if(effectiveDescriptor(declaredTypes.get(source.id())) instanceof CoordinateType - && !sourceCrs.map(MetaAttributeSet.Value::text).equals(targetCrs.map(MetaAttributeSet.Value::text))) { - var location=targetCrs.map(MetaAttributeSet.Value::origin).filter(Origin.DeclaredOrigin.class::isInstance).map(Origin.DeclaredOrigin.class::cast) - .map(Origin.DeclaredOrigin::range).orElseGet(()->target.origin() instanceof Origin.DeclaredOrigin declared?declared.range():translationDecl.syntax().range()); - emitRule(translationDecl,"translation coordinate reference systems differ","TRANSLATION_COORDINATE",location); - } - if (source instanceof ViewSymbol a && target instanceof ViewSymbol b) { - if (!sameMapped(a.baseView(), b.baseView(), correspondence) - || !viewFormationShape(a).equals(viewFormationShape(b)) - || !sameExpressionLists(a.selections(), b.selections(), correspondence) - || !sameExpressionLists(formationExpressions(a), formationExpressions(b), correspondence) - || !a.baseExtensions().stream().map(e -> List.of(correspondence.getOrDefault(e.base(), e.base()), - e.extensions().stream().map(id -> correspondence.getOrDefault(id, id)).toList())).toList() - .equals(b.baseExtensions().stream().map(e -> List.of(e.base(), e.extensions())).toList())) - emitRule(translationDecl, "translation view formation or selection differs", "TRANSLATION_REFERENCE"); - } - if (source instanceof ViewSymbol a && target instanceof ViewSymbol b - && a.formation().orElse(null) instanceof ViewSymbol.Inspection x - && b.formation().orElse(null) instanceof ViewSymbol.Inspection y - && (!sameExpressionLists(List.of(x.path()),List.of(y.path()),correspondence) - || !sameInspectionTarget(x.element(),y.element(),correspondence) - || !sameInspectionTarget(x.parent(),y.parent(),correspondence))) - emitRule(translationDecl,"translation inspection context differs","TRANSLATION_REFERENCE"); - if (source instanceof ViewSymbol.Base a && target instanceof ViewSymbol.Base b - && (a.orNull() != b.orNull() || !correspondence.getOrDefault(a.target(), a.target()).equals(b.target()))) - emitRule(translationDecl, "translation view basis differs", "TRANSLATION_REFERENCE"); - if (source instanceof guru.interlis.model.GraphicSymbol a && target instanceof guru.interlis.model.GraphicSymbol b - && (!sameMapped(a.baseGraphic(),b.baseGraphic(),correspondence) || !sameMapped(a.basis(),b.basis(),correspondence) - || !sameExpressionLists(a.selections(),b.selections(),correspondence))) - emitRule(translationDecl,"translated graphic basis or selections differ","TRANSLATION_GRAPHIC"); - if (source instanceof guru.interlis.model.DrawingRuleSymbol a && target instanceof guru.interlis.model.DrawingRuleSymbol b) { - boolean same=sameMapped(a.signatureClass(),b.signatureClass(),correspondence) && sameMapped(a.overrides(),b.overrides(),correspondence) - && a.clauses().size()==b.clauses().size(); - for(int i=0;same && i correspondence.getOrDefault(id, id)).toList() - .equals(right.restrictions()); - } - if (!same) { - emitRule(translationDecl, "translation role target differs", - "TRANSLATION_REFERENCE"); - } - } - if (source instanceof guru.interlis.model.MetadataBasketSymbol a && target instanceof guru.interlis.model.MetadataBasketSymbol b - && (a.basketKind() != b.basketKind() || !sameMapped(a.topic(), b.topic(), correspondence) || !sameMapped(a.baseBasket(), b.baseBasket(), correspondence))) - emitRule(translationDecl, "translated metadata basket has different bindings", "TRANSLATION_METADATA_BASKET"); - if (source instanceof guru.interlis.model.MetaObjectSymbol a && target instanceof guru.interlis.model.MetaObjectSymbol b - && (!a.name().equals(b.name()) || !sameMapped(a.objectClass(), b.objectClass(), correspondence))) - emitRule(translationDecl, "translated metadata object has a different name or class", "TRANSLATION_METAOBJECT"); - if (source instanceof guru.interlis.model.LineFormSymbol a && target instanceof guru.interlis.model.LineFormSymbol b - && !sameMapped(a.segmentStructure(), b.segmentStructure(), correspondence)) - emitRule(translationDecl, "translated line form targets a different segment structure", "TRANSLATION_LINE_FORM"); - if (source instanceof UnitSymbol sourceUnit && target instanceof UnitSymbol targetUnit) { - boolean same = sourceUnit.abstractUnit() == targetUnit.abstractUnit() - && sameMapped(sourceUnit.base(), targetUnit.base(), correspondence) - && sameMapped(sourceUnit.derivedFrom(), targetUnit.derivedFrom(), correspondence) - && sourceUnit.components().size() == targetUnit.components().size() - && sameUnitConversion(sourceUnit, targetUnit); - if (same) for (int i = 0; i < sourceUnit.components().size(); i++) { - var a = sourceUnit.components().get(i); var b = targetUnit.components().get(i); - if (a.operator() != b.operator() || !sameMapped(Optional.of(a.unit()), Optional.of(b.unit()), correspondence)) same = false; - } - if (!same) { - emitRule(translationDecl, "translation unit base differs", - "TRANSLATION_REFERENCE"); - } - } - if (source instanceof guru.interlis.model.ContextSymbol original && target instanceof guru.interlis.model.ContextSymbol translated) { - boolean same = original.mappings().size() == translated.mappings().size(); - for (int i = 0; same && i < original.mappings().size(); i++) { - var left = original.mappings().get(i); var right = translated.mappings().get(i); - same = correspondence.getOrDefault(left.genericDomain(), left.genericDomain()).equals(right.genericDomain()) - && left.alternatives().stream().map(id -> correspondence.getOrDefault(id, id)).toList().equals(right.alternatives()); - } - if (!same) emitRule(translationDecl, "translation context targets differ", "TRANSLATION_REFERENCE"); - } - if (source instanceof TopicSymbol original && target instanceof TopicSymbol translated - && !original.deferredGenerics().stream().map(id -> correspondence.getOrDefault(id, id)).toList().equals(translated.deferredGenerics())) - emitRule(translationDecl, "translation deferred domains differ", "TRANSLATION_REFERENCE"); - if (source instanceof DomainSymbol sourceDomain - && target instanceof DomainSymbol targetDomain) { - if (!sameMapped(sourceDomain.baseDomain(), targetDomain.baseDomain(), - correspondence)) { - emitRule(translationDecl, "translation domain base differs", - "TRANSLATION_REFERENCE"); - } - if (!sameMappedType(sourceDomain.typeId(), targetDomain.typeId(), - correspondence)) { - emitRule(translationDecl, "translation domain type differs", - "TRANSLATION_REFERENCE"); - } - } - } - - private boolean sameExpressionLists(List source, List target, - Map correspondence) { - if (source.size() != target.size()) return false; - for (int i = 0; i < source.size(); i++) { - checkWork(); - if (!sameExpression(source.get(i),target.get(i),correspondence)) return false; - var expected = expressionReferenceTargets(source.get(i)).stream() - .map(id -> correspondence.getOrDefault(id, id)).toList(); - if (!expected.equals(expressionReferenceTargets(target.get(i)))) return false; - } - return true; - } - - private List formationExpressions(ViewSymbol view) { - return view.formation().map(f -> switch (f) { - case ViewSymbol.Aggregation a -> a.equal(); - case ViewSymbol.Inspection i -> List.of(i.path()); - default -> List.of(); - }).orElse(List.of()); - } - - private String viewFormationShape(ViewSymbol view) { - return view.formation().map(f -> switch (f) { - case ViewSymbol.Projection ignored -> "PROJECTION"; - case ViewSymbol.Join j -> "JOIN:" + j.bases().size(); - case ViewSymbol.Union u -> "UNION:" + u.bases().size(); - case ViewSymbol.Aggregation a -> "AGGREGATION:" + a.equal().size(); - case ViewSymbol.Inspection i -> "INSPECTION:" + i.area(); - }).orElse("NONE"); - } - - private boolean sameMapped(Optional source, Optional target, - Map correspondence) { - if (source.isEmpty() || target.isEmpty()) { - return source.isEmpty() && target.isEmpty(); - } - SymbolId mapped = correspondence.getOrDefault(source.orElseThrow(), source.orElseThrow()); - return mapped.equals(target.orElseThrow()); - } - - private boolean sameAttributePathOwner(Optional a, - Optional b, Map correspondence) { - if (a.isEmpty() || b.isEmpty()) return a.isEmpty() && b.isEmpty(); - if (a.orElseThrow() instanceof guru.interlis.model.AttributePathType.ClassAttribute x - && b.orElseThrow() instanceof guru.interlis.model.AttributePathType.ClassAttribute y) - return sameExpressionLists(List.of(x.path()), List.of(y.path()), correspondence); - if (a.orElseThrow() instanceof guru.interlis.model.AttributePathType.Argument x - && b.orElseThrow() instanceof guru.interlis.model.AttributePathType.Argument y) - return x.index() == y.index() && sameMapped(Optional.of(x.function()), Optional.of(y.function()), correspondence); - return a.orElseThrow() instanceof guru.interlis.model.AttributePathType.Viewable x - && b.orElseThrow() instanceof guru.interlis.model.AttributePathType.Viewable y - && sameMapped(Optional.of(x.target()), Optional.of(y.target()), correspondence); - } - - private boolean sameMappedType(TypeId source, TypeId target, - Map correspondence) { - checkWork(); - TypeDescriptor sourceType = types.get(source); - TypeDescriptor targetType = types.get(target); - if(sourceType==null || targetType==null || sourceType.getClass()!=targetType.getClass())return false; - if(sourceType instanceof TextType a && targetType instanceof TextType b) - return a.textKind()==b.textKind() && a.maxLength().equals(b.maxLength()); - if(sourceType instanceof AlignmentType a && targetType instanceof AlignmentType b)return a.alignment()==b.alignment(); - if(sourceType instanceof BlackboxType a && targetType instanceof BlackboxType b)return a.blackboxKind()==b.blackboxKind(); - if(sourceType instanceof guru.interlis.model.EnumArgumentType a && targetType instanceof guru.interlis.model.EnumArgumentType b)return a.treeValues()==b.treeValues(); - if(sourceType instanceof guru.interlis.model.GenericType a && targetType instanceof guru.interlis.model.GenericType b)return a.name().equals(b.name()); - if(sourceType instanceof EnumerationType a && targetType instanceof EnumerationType b) - return a.ordered()==b.ordered() && a.circular()==b.circular() && a.treeValues()==b.treeValues() - && sameEnumBase(a.baseType(),b.baseType(),correspondence) && enumParents(a).equals(enumParents(b)) && enumFinals(a).equals(enumFinals(b)); - if(sourceType instanceof FormattedType a && targetType instanceof FormattedType b) { - if(!a.predefined().equals(b.predefined()) || !sameMapped(a.baseDomain(),b.baseDomain(),correspondence) - || !sameMapped(a.structure(),b.structure(),correspondence) || !a.minimum().equals(b.minimum()) - || !a.maximum().equals(b.maximum()) || a.parts().size()!=b.parts().size())return false; - for(int i=0;i correspondence.getOrDefault(id, id)).toList().equals(b.restrictions()); - if (sourceType instanceof ClassValueType a && targetType instanceof ClassValueType b) - return a.structureValue() == b.structureValue() && a.restrictions().stream() - .map(id -> correspondence.getOrDefault(id, id)).toList().equals(b.restrictions()); - if (sourceType instanceof ReferenceType sourceReference - && targetType instanceof ReferenceType targetReference) { - return sourceReference.external() == targetReference.external() - && sameMapped(sourceReference.target(), targetReference.target(), correspondence) - && sourceReference.restrictions().size() == targetReference.restrictions().size() - && java.util.stream.IntStream.range(0, sourceReference.restrictions().size()).allMatch(i -> - sameMapped(Optional.of(sourceReference.restrictions().get(i)), - Optional.of(targetReference.restrictions().get(i)), correspondence)); - } - if (sourceType instanceof StructureValueType sourceStructure - && targetType instanceof StructureValueType targetStructure) { - return sameMapped(sourceStructure.target(), targetStructure.target(), correspondence) - && sourceStructure.restrictions().stream().map(t -> correspondence.getOrDefault(t,t)).toList().equals(targetStructure.restrictions()); - } - if (sourceType instanceof DomainReferenceType sourceDomain - && targetType instanceof DomainReferenceType targetDomain) { - return sameMapped(Optional.of(sourceDomain.targetDomain()), - Optional.of(targetDomain.targetDomain()), correspondence); - } - if (sourceType instanceof CollectionType sourceCollection - && targetType instanceof CollectionType targetCollection) { - return sourceCollection.collectionKind()==targetCollection.collectionKind() - && sourceCollection.cardinality().equals(targetCollection.cardinality()) - && sameMappedType(sourceCollection.elementType(), targetCollection.elementType(), - correspondence); - } - if (sourceType instanceof GeometryType sourceGeometry - && targetType instanceof GeometryType targetGeometry) { - return sourceGeometry.geometryKind()==targetGeometry.geometryKind() - && sameMapped(sourceGeometry.vertexDomain(), targetGeometry.vertexDomain(), correspondence) - && sameMapped(sourceGeometry.lineAttributes(), targetGeometry.lineAttributes(), correspondence) - && sourceGeometry.directed() == targetGeometry.directed() - && sourceGeometry.withoutOverlaps() == targetGeometry.withoutOverlaps() - && sameGeometryTolerance(sourceGeometry.overlap(), targetGeometry.overlap(), correspondence) - && sourceGeometry.lineForms().stream().map(x -> correspondence.getOrDefault(x, x)) - .collect(java.util.stream.Collectors.toSet()).equals(new HashSet<>(targetGeometry.lineForms())); - } - if (sourceType instanceof NumericType a && targetType instanceof NumericType b) - return sameNumeric(a.range(), b.range(), correspondence); - if (sourceType instanceof CoordinateType sourceCoordinate - && targetType instanceof CoordinateType targetCoordinate) { - if (sourceCoordinate.multiCoordinate()!=targetCoordinate.multiCoordinate() || !sourceCoordinate.rotation().equals(targetCoordinate.rotation()) || sourceCoordinate.dimensions() != targetCoordinate.dimensions() - || !sourceCoordinate.referenceSystem().equals( - targetCoordinate.referenceSystem())) { - return false; - } - for (int i = 0; i < sourceCoordinate.axes().size(); i++) { - checkWork(); - NumericRange sourceAxis = sourceCoordinate.axes().get(i); - NumericRange targetAxis = targetCoordinate.axes().get(i); - if (!sameNumeric(sourceAxis, targetAxis, correspondence)) { - return false; - } - } - return true; - } - return sourceType instanceof BooleanType; - } - - private boolean sameEnumBase(Optional source,Optional target,Map map) { - if(source.isEmpty() || target.isEmpty())return source.isEmpty() && target.isEmpty(); - var a=declaredTypes.entrySet().stream().filter(e->e.getValue().equals(source.orElseThrow())).map(Map.Entry::getKey).findFirst(); - var b=declaredTypes.entrySet().stream().filter(e->e.getValue().equals(target.orElseThrow())).map(Map.Entry::getKey).findFirst(); - return a.isPresent() && b.isPresent() ? sameMapped(a,b,map) : sameMappedType(source.orElseThrow(),target.orElseThrow(),map); - } - - /** Enum shape uses parent ordinals, never translated names or an arbitrary nesting limit. */ - private List enumParents(EnumerationType enumeration) { - var paths=enumeration.values().stream().map(EnumerationValue::path).toList();var result=new ArrayList(); - for(var path:paths){checkWork();int dot=path.lastIndexOf('.');result.add(dot<0?-1:paths.indexOf(path.substring(0,dot)));} - return result; - } - private Set enumFinals(EnumerationType enumeration) { - var paths=enumeration.values().stream().map(EnumerationValue::path).toList();var result=new HashSet(); - for(var path:enumeration.finalSubtrees()){checkWork();result.add(path.isEmpty()?-1:paths.indexOf(path));} - return result; - } - - private boolean sameDecimal(Optional a,Optional b) { - if(a.isEmpty() || b.isEmpty())return a.isEmpty() && b.isEmpty(); - var x=a.orElseThrow();var y=b.orElseThrow(); - return x.exactValue().compareTo(y.exactValue())==0 - && (long)x.precision().exponent()-x.precision().fractionalDigits()==(long)y.precision().exponent()-y.precision().fractionalDigits(); - } - - private boolean sameGeometryTolerance(Optional source, Optional target, - Map correspondence) { - if (source.orElse(null) instanceof GeometryType.TrackWidth a && target.orElse(null) instanceof GeometryType.TrackWidth b) - return correspondence.getOrDefault(a.coordinateDomain(), a.coordinateDomain()).equals(b.coordinateDomain()); - if (source.orElse(null) instanceof GeometryType.Explicit a && target.orElse(null) instanceof GeometryType.Explicit b) - return a.value().exactValue().compareTo(b.value().exactValue()) == 0; - return source.isEmpty() && target.isEmpty(); - } - - private boolean sameNumeric(NumericRange a, NumericRange b, Map map) { - if (!sameDecimal(a.minimum(),b.minimum()) || !sameDecimal(a.maximum(),b.maximum()) || a.circular() != b.circular() - || a.direction() != b.direction() || !sameMapped(a.unit(), b.unit(), map) || a.referenceSystem().isPresent() != b.referenceSystem().isPresent()) return false; - return sameReferenceSystem(a.referenceSystem(),b.referenceSystem(),map); - } - private boolean sameReferenceSystem(Optional a,Optional b,Map map) { - if(a.isEmpty() || b.isEmpty())return a.isEmpty() && b.isEmpty(); - var x=a.orElseThrow();var y=b.orElseThrow(); - return x.domain() == y.domain() && x.axis().equals(y.axis()) && map.getOrDefault(x.target(), x.target()).equals(y.target()); - } - - private List constraintReferenceTargets(ConstraintDefinition definition) { - List ids = new ArrayList<>(); - List targets = new ArrayList<>(); - switch (definition) { - case MandatoryConstraint c -> ids.add(c.condition()); - case DomainConstraint c -> ids.add(c.condition()); - case PlausibilityConstraint c -> ids.add(c.condition()); - case guru.interlis.model.SetConstraint c -> { c.where().ifPresent(ids::add); ids.add(c.condition()); } - case UniquenessConstraint c -> { - c.where().ifPresent(ids::add); ids.addAll(c.keys()); - for (var local : c.localKeys()) { ids.add(local.prefix()); ids.addAll(local.fields()); } - } - case guru.interlis.model.ExistenceConstraint c -> { - ids.add(c.source()); - for (var target : c.targets()) { targets.add(target.viewable()); ids.add(target.path()); } - } - } - for (var id : ids) targets.addAll(expressionReferenceTargets(id)); - return targets; - } - - private List expressionReferenceTargets(ExpressionId id) { - if (id == null) { - return List.of(); - } - TypedExpression expression = expressions.get(id); - if (expression == null) { - return List.of(); - } - List targets = new ArrayList<>(); - if (expression instanceof guru.interlis.model.PathExpression path) { - for (guru.interlis.model.PathStep step : path.path().steps()) { - checkWork(); - if (step instanceof guru.interlis.model.PathStep.ViewBaseStep base) { - targets.add(base.base()); targets.add(base.target()); - } else if (step instanceof guru.interlis.model.PathStep.InspectionStep inspected) { - targets.add(inspected.view()); targets.add(inspected.target()); - } else if (step instanceof guru.interlis.model.PathStep.AttributeStep attribute) { - targets.add(attribute.target()); - } else if (step instanceof guru.interlis.model.PathStep.RoleStep role) { - targets.add(role.target()); - } else if (step instanceof guru.interlis.model.PathStep.AssociationAccessStep access) { - targets.add(access.association()); targets.add(access.sourceRole()); targets.add(access.targetRole()); - } else if (step instanceof guru.interlis.model.PathStep.AssociationSourceStep access) { - targets.add(access.association()); targets.add(access.sourceRole()); - } - } - } - if(expression instanceof guru.interlis.model.FunctionCallExpression call) { - call.targetFunction().ifPresent(targets::add); - for(var path:call.argumentPaths().values())path.binding().ifPresent(id2->targets.addAll(expressionReferenceTargets(id2))); - } - if (expression instanceof guru.interlis.model.SymbolValueExpression value) targets.add(value.target()); - if (expression instanceof guru.interlis.model.InspectionExpression inspected) { - inspected.view().ifPresent(targets::add); targets.add(inspected.source()); targets.add(inspected.element()); - } - if (expression instanceof guru.interlis.model.ObjectSetExpression - && types.get(expression.typeId()) instanceof guru.interlis.model.ObjectArgumentType set) { - set.base().ifPresent(targets::add); targets.addAll(set.restrictions()); - } - for (ExpressionId child : expression.children()) { - checkWork(); - targets.addAll(expressionReferenceTargets(child)); - } - return targets; - } - - private boolean sameConstraint(ConstraintDefinition source,ConstraintDefinition target,Map map) { - if(source.getClass()!=target.getClass())return false; - if(!sameExpressionLists(constraintExpressions(source),constraintExpressions(target),map))return false; - if(source instanceof PlausibilityConstraint a && target instanceof PlausibilityConstraint b - && (a.direction()!=b.direction() || a.percentage().compareTo(b.percentage())!=0))return false; - if(source instanceof guru.interlis.model.SetConstraint a && target instanceof guru.interlis.model.SetConstraint b - && (a.scope()!=b.scope() || a.where().isPresent()!=b.where().isPresent()))return false; - if(source instanceof UniquenessConstraint a && target instanceof UniquenessConstraint b - && (a.basket()!=b.basket() || a.where().isPresent()!=b.where().isPresent() - || a.keys().size()!=b.keys().size() || a.localKeys().size()!=b.localKeys().size() - || !a.localKeys().stream().map(k->k.fields().size()).toList().equals(b.localKeys().stream().map(k->k.fields().size()).toList())))return false; - if(source instanceof guru.interlis.model.ExistenceConstraint a && target instanceof guru.interlis.model.ExistenceConstraint b - && a.targets().size()!=b.targets().size())return false; - var originalTargets=constraintReferenceTargets(source).stream().map(id->map.getOrDefault(id,id)).toList(); - if(!originalTargets.equals(constraintReferenceTargets(target)))return false; - return true; - } - - private boolean constraintHasErrors(ConstraintDefinition definition) { - return constraintExpressions(definition).stream().map(expressions::get) - .anyMatch(e->e!=null&&e.typeState()==guru.interlis.model.ExpressionTypeState.ERROR); - } - - private List constraintExpressions(ConstraintDefinition definition) { - var ids=new ArrayList(); - switch(definition) { - case MandatoryConstraint c -> ids.add(c.condition()); - case DomainConstraint c -> ids.add(c.condition()); - case PlausibilityConstraint c -> ids.add(c.condition()); - case guru.interlis.model.SetConstraint c -> {c.where().ifPresent(ids::add);ids.add(c.condition());} - case UniquenessConstraint c -> {c.where().ifPresent(ids::add);ids.addAll(c.keys());for(var k:c.localKeys()){ids.add(k.prefix());ids.addAll(k.fields());}} - case guru.interlis.model.ExistenceConstraint c -> {ids.add(c.source());for(var t:c.targets())ids.add(t.path());} - } - return ids; - } - - /** Structural expression comparison uses bound identities; documentary spelling is irrelevant. */ - boolean sameBoundExpression(ExpressionId source,ExpressionId target) { - return sameExpression(source,target,Map.of()); - } - - private boolean sameExpression(ExpressionId source,ExpressionId target,Map map) { - checkWork();var a=expressions.get(source);var b=expressions.get(target); - if(a==null || b==null || a.getClass()!=b.getClass())return false; - if(a instanceof guru.interlis.model.LiteralExpression x && b instanceof guru.interlis.model.LiteralExpression y) { - if(x.literalKind()!=y.literalKind())return false; - return switch(x.literalKind()) { - case NUMBER -> x.decimal().isPresent() && y.decimal().isPresent() - && x.decimal().orElseThrow().exactValue().compareTo(y.decimal().orElseThrow().exactValue())==0 - && sameNumericContext(x.typeId(),y.typeId(),map); - case TEXT -> x.textValue().equals(y.textValue()); - case BOOLEAN -> x.lexeme().equals(y.lexeme()); - }; - } - if(a instanceof guru.interlis.model.BinaryExpression x && b instanceof guru.interlis.model.BinaryExpression y && x.operator()!=y.operator())return false; - if(a instanceof guru.interlis.model.UnaryExpression x && b instanceof guru.interlis.model.UnaryExpression y && x.operator()!=y.operator())return false; - if(a instanceof guru.interlis.model.FunctionCallExpression x && b instanceof guru.interlis.model.FunctionCallExpression y) { - if(!sameMapped(x.targetFunction(),y.targetFunction(),map) - || x.targetFunction().isEmpty() && !x.functionName().equals(y.functionName()) - || !x.argumentPaths().keySet().equals(y.argumentPaths().keySet()) || x.children().size()!=y.children().size())return false; - for(int i=0;iList.of(v.name(),v.associationAccess(),v.associationQualifier(),v.index())).toList() - .equals(q.steps().stream().map(v->List.of(v.name(),v.associationAccess(),v.associationQualifier(),v.index())).toList()))return false; - } - } - return true; - } - if(a instanceof guru.interlis.model.PathExpression x && b instanceof guru.interlis.model.PathExpression y) { - if(x.path().steps().size()!=y.path().steps().size())return false; - for(int i=0;i map) { - var a=effectiveDescriptor(source);var b=effectiveDescriptor(target); - if(a instanceof NumericType x && b instanceof NumericType y) - return sameMapped(x.range().unit(),y.range().unit(),map) - && sameReferenceSystem(x.range().referenceSystem(),y.range().referenceSystem(),map); - return a!=null && b!=null && a.getClass()==b.getClass(); - } - private int enumValueIndex(EnumerationType enumeration,String path) { - return enumeration.values().stream().map(EnumerationValue::path).toList().indexOf(path); - } - private boolean samePathStep(guru.interlis.model.PathStep source,guru.interlis.model.PathStep target,Map map) { - checkWork();if(source.getClass()!=target.getClass())return false; - return switch(source) { - case guru.interlis.model.PathStep.AttributeStep a -> sameInspectionTarget(a.target(),((guru.interlis.model.PathStep.AttributeStep)target).target(),map); - case guru.interlis.model.PathStep.RoleStep a -> sameInspectionTarget(a.target(),((guru.interlis.model.PathStep.RoleStep)target).target(),map); - case guru.interlis.model.PathStep.IndexStep a -> a.index().equals(((guru.interlis.model.PathStep.IndexStep)target).index()); - case guru.interlis.model.PathStep.ViewBaseStep a -> {var b=(guru.interlis.model.PathStep.ViewBaseStep)target;yield sameInspectionTarget(a.base(),b.base(),map) && sameInspectionTarget(a.target(),b.target(),map);} - case guru.interlis.model.PathStep.InspectionStep a -> {var b=(guru.interlis.model.PathStep.InspectionStep)target;yield a.kind()==b.kind() && sameInspectionTarget(a.view(),b.view(),map) && sameInspectionTarget(a.target(),b.target(),map);} - case guru.interlis.model.PathStep.AssociationAccessStep a -> {var b=(guru.interlis.model.PathStep.AssociationAccessStep)target;yield a.associationObject()==b.associationObject() && a.cardinality().equals(b.cardinality()) - && sameInspectionTarget(a.association(),b.association(),map) && sameInspectionTarget(a.sourceRole(),b.sourceRole(),map) && sameInspectionTarget(a.targetRole(),b.targetRole(),map);} - case guru.interlis.model.PathStep.AssociationSourceStep a -> {var b=(guru.interlis.model.PathStep.AssociationSourceStep)target;yield a.cardinality().equals(b.cardinality()) && sameInspectionTarget(a.association(),b.association(),map) && sameInspectionTarget(a.sourceRole(),b.sourceRole(),map);} - case guru.interlis.model.PathStep.ThisStep ignored -> true; - case guru.interlis.model.PathStep.ParentStep ignored -> true; - case guru.interlis.model.PathStep.UnresolvedStep ignored -> false; - }; - } - - /** A registered geometry context may extend correspondence only after its actual - * bound geometry agrees. No generated identifier or display name is interpreted. */ - private boolean sameInspectionTarget(SymbolId source, SymbolId target, Map map) { - if (map.getOrDefault(source,source).equals(target)) return true; - var a=views.geometryContext(source);var b=views.geometryContext(target); - if (a==null || b==null - || !map.getOrDefault(a.segments().contextOwner(),a.segments().contextOwner()).equals(b.segments().contextOwner()) - || !sameMappedType(a.geometry().id(),b.geometry().id(),map)) return false; - var left=a.segments().contextualSymbols();var right=b.segments().contextualSymbols(); - if(left.size()!=right.size())return false; - var candidate=new LinkedHashMap<>(map); - for(var pair:left.entrySet()) { - checkWork(); - var original=map.getOrDefault(pair.getKey(),pair.getKey()); - var translated=right.get(original); - if(translated==null)return false; - var previous=candidate.putIfAbsent(pair.getValue(),translated); - if(previous!=null && !previous.equals(translated))return false; - } - var leftWrappers=views.geometryWrappers(a);var rightWrappers=views.geometryWrappers(b); - if(!leftWrappers.keySet().equals(rightWrappers.keySet()))return false; - for(var role:leftWrappers.keySet()) { - checkWork(); - var original=symbols.get(leftWrappers.get(role));var translated=symbols.get(rightWrappers.get(role)); - if(original==null || translated==null || !correspondenceInto(original,translated,candidate))return false; - } - if(!candidate.getOrDefault(source,source).equals(target))return false; - // New context copies are bijective. Existing ordinary correspondences are - // never replaced, and two different copies cannot collapse onto one target. - var usedTargets = new HashMap(); - map.forEach((original,translated) -> usedTargets.putIfAbsent(translated,original)); - for(var pair:candidate.entrySet()) if(!map.containsKey(pair.getKey())) { - checkWork();var previous=usedTargets.putIfAbsent(pair.getValue(),pair.getKey()); - if(previous!=null && !previous.equals(pair.getKey()))return false; - } - // Validation is transactional: a rejected context cannot poison later comparisons. - candidate.forEach(map::putIfAbsent); - return true; - } - - private boolean sameUnitConversion(UnitSymbol left, UnitSymbol right) { - if (left.conversionFunction().isPresent() != right.conversionFunction().isPresent() - || left.conversionFactors().size() != right.conversionFactors().size()) return false; - for (int i = 0; i < left.conversionFactors().size(); i++) { - checkWork(); - var a = left.conversionFactors().get(i); var b = right.conversionFactors().get(i); - if (a.operator() != b.operator() || a.kind() != b.kind() - || a.decimal().isPresent() != b.decimal().isPresent() - || a.decimal().isPresent() && a.decimal().get().exactValue().compareTo(b.decimal().get().exactValue()) != 0) - return false; - } - return true; - } - - private boolean sameDrawingValue(guru.interlis.model.DrawingRuleSymbol.Value left,guru.interlis.model.DrawingRuleSymbol.Value right,Map map) { - if(left instanceof guru.interlis.model.DrawingRuleSymbol.ExpressionValue a && right instanceof guru.interlis.model.DrawingRuleSymbol.ExpressionValue b) - return sameExpressionLists(List.of(a.expression()),List.of(b.expression()),map); - if(left instanceof guru.interlis.model.DrawingRuleSymbol.MetaObjectValue a && right instanceof guru.interlis.model.DrawingRuleSymbol.MetaObjectValue b) - return sameMapped(a.object(),b.object(),map); - if(left instanceof guru.interlis.model.DrawingRuleSymbol.AccordingValue a && right instanceof guru.interlis.model.DrawingRuleSymbol.AccordingValue b) { - if(!sameExpressionLists(List.of(a.selector()),List.of(b.selector()),map) || a.cases().size()!=b.cases().size()) return false; - var at=effectiveDescriptor(expressions.get(a.selector()).typeId()); var bt=effectiveDescriptor(expressions.get(b.selector()).typeId()); - if(!(at instanceof EnumerationType ae && bt instanceof EnumerationType be)) return false; - var ap=ae.values().stream().map(guru.interlis.model.EnumerationValue::path).toList(); var bp=be.values().stream().map(guru.interlis.model.EnumerationValue::path).toList(); - for(int i=0;i extendedBases = new LinkedHashSet<>(); - Map> subclasses = new LinkedHashMap<>(); - for (Symbol symbol : symbols.values()) { - checkWork(); - if (!(symbol instanceof ClassSymbol clazz) || clazz.baseType().isEmpty()) { - continue; - } - SymbolId base = clazz.baseType().orElseThrow(); - subclasses.computeIfAbsent(base, ignored -> new ArrayList<>()).add(clazz.id()); - Symbol baseSymbol = symbols.get(base); - if (baseSymbol != null && baseSymbol.header().modifiers() - .contains(guru.interlis.model.Modifier.EXTENDED)) { - extendedBases.add(base); - } - } - Map extendedByTopic = new LinkedHashMap<>(); - for (Symbol symbol : symbols.values()) { - checkWork(); - if (symbol instanceof ClassSymbol clazz - && clazz.header().modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - SymbolId topic = scopeRoot(clazz.id()); - if (topic != null) { - extendedByTopic.put(topic + ":" + clazz.name().value(), clazz.id()); - } - } - } - for (Symbol symbol : symbols.values()) { - checkWork(); - if (!(symbol instanceof ClassSymbol clazz) || clazz.baseType().isEmpty()) { - continue; - } - SymbolId base = clazz.baseType().orElseThrow(); - Symbol baseSymbol = symbols.get(base); - SymbolId topic = scopeRoot(clazz.id()); - if (baseSymbol != null && topic != null) { - SymbolId localExtended = extendedByTopic.get(topic + ":" + baseSymbol.name().value()); - if (localExtended != null && !localExtended.equals(base) && !localExtended.equals(clazz.id())) { - index.byId(clazz.id()).ifPresent(decl -> - emitRule(decl, "subclass must extend the local EXTENDED class", - "EXTENDED_BASE_CLASS")); - } else if (localExtended != null - && symbols.get(topic) instanceof TopicSymbol topicSymbol - && topicSymbol.baseTopic().isEmpty()) { - index.byId(clazz.id()).ifPresent(decl -> - emitRule(decl, "EXTENDED class may only be extended in a derived topic", - "EXTENDED_BASE_CLASS")); - } - } - } - for (Symbol symbol : symbols.values()) { - checkWork(); - if (!(symbol instanceof ClassSymbol clazz) - || !clazz.header().modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - continue; - } - SymbolId topic = scopeRoot(clazz.id()); - if (!(symbols.get(topic) instanceof TopicSymbol topicSymbol) - || topicSymbol.baseTopic().isEmpty()) { - continue; - } - SymbolId baseTopic = topicSymbol.baseTopic().orElseThrow(); - for (DeclInfo baseChild : index.children(baseTopic)) { - checkWork(); - boolean specialized = index.children(baseTopic).stream() - .filter(child -> child.kind() == SymbolKind.CLASS) - .anyMatch(child -> baseOf.get(child.id()) != null - && baseOf.get(child.id()).equals(baseChild.id())); - if (baseChild.kind() == SymbolKind.CLASS - && baseChild.name().equals(clazz.name().value()) && specialized) { - index.byId(clazz.id()).ifPresent(decl -> - emitRule(decl, "EXTENDED class conflicts with a specialized base class", - "EXTENDED_CLASS_CONFLICT")); - } - } - } - } - - private void validateAttributeOverrides() { - for (Symbol symbol : symbols.values()) { - checkWork(); - if (!(symbol instanceof ClassSymbol) && !(symbol instanceof StructureSymbol) && !(symbol instanceof AssociationSymbol)) { - continue; - } - SymbolId base = baseOf.get(symbol.id()); - if (base == null) { - continue; - } - List members = symbol instanceof ClassSymbol classSymbol - ? classSymbol.members() : symbol instanceof StructureSymbol structure ? structure.members() - : ((AssociationSymbol) symbol).attributes(); - for (SymbolId member : members) { - checkWork(); - Symbol memberSymbol = symbols.get(member); - if (!(memberSymbol instanceof AttributeSymbol attribute)) { - continue; - } - DeclInfo attributeDecl = index.byId(member).orElse(null); - if (attributeDecl == null - || !attributeDecl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - continue; - } - AttributeSymbol baseAttribute = findBaseAttribute(base, attributeDecl.name()); - if (baseAttribute == null) { - continue; - } - validateAttributeOverrideProperties(attributeDecl, attribute, baseAttribute); - TypeDescriptor baseType = effectiveDescriptor(baseAttribute.declaredType()); - TypeDescriptor derivedType = effectiveDescriptor(attribute.declaredType()); - if (baseType != null && derivedType != null) { - String problem = overrideProblem(baseType, derivedType, attributeDecl.language()); - if (problem != null) { - emitRule(attributeDecl, problem, "ATTRIBUTE_OVERRIDE_TYPE"); - } - } - } - } - } - - /** Shared mandatory, transient and domain-identity rules for attribute overrides. */ - void validateAttributeOverrideProperties(DeclInfo attributeDecl, AttributeSymbol attribute, AttributeSymbol baseAttribute) { - if (baseAttribute.mandatory() && !attribute.mandatory()) { - emitRule(attributeDecl, "extended attribute may not remove MANDATORY", - "ATTRIBUTE_OVERRIDE_MANDATORY"); - } - if (baseAttribute.transientAttribute() != attribute.transientAttribute()) { - emitRule(attributeDecl, "extended attribute changes TRANSIENT mode", - "ATTRIBUTE_OVERRIDE_TRANSIENT"); - } - if (types.get(baseAttribute.declaredType()) instanceof DomainReferenceType baseDomainRef - && types.get(attribute.declaredType()) instanceof DomainReferenceType derivedDomainRef - && !domainExtends(derivedDomainRef.targetDomain(), baseDomainRef.targetDomain())) { - emitRule(attributeDecl, "extended attribute changes domain", - "ATTRIBUTE_OVERRIDE_TYPE"); - } - } - - private AttributeSymbol findBaseAttribute(SymbolId base, String name) { - SymbolId current = base; - Set seen = new HashSet<>(); - while (current != null && seen.add(current)) { - checkWork(); - Symbol symbol = symbols.get(current); - List members = symbol instanceof ClassSymbol classSymbol - ? classSymbol.members() - : symbol instanceof StructureSymbol structure ? structure.members() - : symbol instanceof AssociationSymbol association ? association.attributes() : List.of(); - for (SymbolId member : members) { - checkWork(); - Symbol memberSymbol = symbols.get(member); - if (memberSymbol instanceof AttributeSymbol attribute && attribute.name().value().equals(name)) { - return attribute; - } - } - current = baseOf.get(current); - } - return null; - } - - private boolean domainExtends(SymbolId derived, SymbolId base) { - SymbolId current = derived; - Set visited = new LinkedHashSet<>(); - while (current != null && visited.add(current)) { - checkWork(); - if (current.equals(base)) { - return true; - } - current = baseOf.get(current); - } - return false; - } - - private String overrideProblem(TypeDescriptor base, TypeDescriptor derived, guru.interlis.model.LanguageVersion language) { - var relation=extension(derived,base,language); - return relation.decision()==guru.interlis.model.RelationDecision.ALLOWED?null:relation.reason(); - } - - private guru.interlis.model.TypeRelation extension(TypeDescriptor derived, TypeDescriptor base, guru.interlis.model.LanguageVersion language) { - return guru.interlis.model.internal.TypeCompatibility.extend(derived,base,language,this::effectiveDescriptor, - baseOf::get,id->effectiveDescriptor(declaredTypes.get(id)),this::extensionUnit); - } - - private void validateAssociationRules() { - for (Symbol symbol : symbols.values()) { - checkWork(); - if (symbol instanceof AssociationSymbol association) { - Set roleNames = new LinkedHashSet<>(); - for (SymbolId roleId : association.roles()) { - checkWork(); - Symbol role = symbols.get(roleId); - if (role != null) { - roleNames.add(role.name().value()); - } - } - for (SymbolId memberId : association.attributes()) { - checkWork(); - Symbol member = symbols.get(memberId); - if (member != null && roleNames.contains(member.name().value())) { - DeclInfo associationDecl = index.byId(association.id()).orElse(null); - if (associationDecl != null) { - emitRule(associationDecl, - "role and attribute share a name in the association", - "ROLE_ATTRIBUTE_CONFLICT"); - } - } - } - } - } - Map> roleNamesByTarget = new LinkedHashMap<>(); - for (AssociationSymbol association : symbols.values().stream() - .filter(AssociationSymbol.class::isInstance) - .map(AssociationSymbol.class::cast).toList()) { - checkWork(); - DeclInfo decl = index.byId(association.id()).orElse(null); - if (decl == null) { - continue; - } - guru.interlis.syntax.AssociationDeclarationSyntax syntax = - (guru.interlis.syntax.AssociationDeclarationSyntax) decl.syntax(); - List effectiveRoles = associationRoles(association.id()); - if (effectiveRoles.size() < 2) emitRule(decl, "association requires at least two effective roles", "ASSOCIATION_ROLES"); - for (RoleSymbol role : effectiveRoles) { - if (role.targetRoots().stream().anyMatch(t -> targetSpecializes(association.id(), t) - && index.byId(t).map(DeclInfo::kind).orElse(null) == SymbolKind.ASSOCIATION)) { - emitRule(decl, "association may not specialize a role target", "ASSOCIATION_TARGET_BASE"); - } - } - for (RoleSymbol hiding : effectiveRoles) { - if (!hiding.hiding()) continue; - for (Symbol other : symbols.values()) { - checkWork(); - if (!(other instanceof AssociationSymbol sibling) - || baseChain(association.id()).contains(sibling.id()) - || baseChain(sibling.id()).contains(association.id()) - || baseChain(association.id()).stream().noneMatch(baseChain(sibling.id())::contains)) continue; - for (RoleSymbol role : associationRoles(sibling.id())) { - if (!role.hiding() && role.name().equals(hiding.name()) - && role.targetRoots().stream().anyMatch(t -> hiding.targetRoots().stream() - .anyMatch(h -> targetSpecializes(t, h) || targetSpecializes(h, t)))) { - emitRule(decl, "HIDING conflicts with a non-hiding sibling role", "ROLE_HIDING_CONFLICT"); - } - } - } - } - long aggregations = effectiveRoles.stream() - .filter(role -> role.aggregation() != RoleAggregation.NONE).count(); - if (aggregations > 1) { - emitRule(decl, "only one aggregation role is allowed", "MULTIPLE_AGGREGATION"); - } - if (effectiveRoles.size()!=2 && effectiveRoles.stream().anyMatch(role->role.aggregation()==RoleAggregation.AGGREGATION)) { - emitRule(decl, "aggregation requires exactly two effective roles", "AGGREGATION_ARITY"); - } - boolean associationAbstract = syntax.modifiers() - .contains(guru.interlis.model.Modifier.ABSTRACT); - for (guru.interlis.syntax.RoleDeclarationSyntax roleSyntax : syntax.roles()) { - checkWork(); - if (roleSyntax.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT) - && !associationAbstract) { - DeclInfo roleDecl = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.ROLE - && child.name().equals(roleSyntax.name())) - .findFirst().orElse(decl); - emitRule(roleDecl, "abstract role requires an abstract association", - "ABSTRACT_ROLE"); - } - if (roleSyntax.aggregation() != RoleAggregation.COMPOSITION - || !roleSyntax.explicitCardinality()) { - continue; - } - boolean one = roleSyntax.cardinality().lower().equals(java.math.BigInteger.ONE) - && roleSyntax.cardinality().upper() - instanceof guru.interlis.model.CardinalityUpper.Finite finite - && finite.value().equals(java.math.BigInteger.ONE); - if (!one) { - DeclInfo roleDecl = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.ROLE - && child.name().equals(roleSyntax.name())) - .findFirst().orElse(decl); - emitRule(roleDecl, "composition role cardinality must be 1", "COMPOSITION_CARDINALITY"); - } - } - List roles = association.roles().stream() - .map(symbols::get).filter(RoleSymbol.class::isInstance) - .map(RoleSymbol.class::cast).toList(); - if (roles.size() == 2) { - roles.get(0).targetRoots().forEach(t -> addRoleNamespace(roleNamesByTarget, t, roles.get(1))); - roles.get(1).targetRoots().forEach(t -> addRoleNamespace(roleNamesByTarget, t, roles.get(0))); - } - } - for (ClassSymbol classSymbol : symbols.values().stream() - .filter(ClassSymbol.class::isInstance).map(ClassSymbol.class::cast).toList()) { - checkWork(); - Set chain = new LinkedHashSet<>(); - SymbolId current = classSymbol.id(); - while (current != null && chain.add(current)) { - checkWork(); - current = baseOf.get(current); - } - Map attributes = new LinkedHashMap<>(); - for (SymbolId ancestor : chain) for (DeclInfo member : index.children(ancestor)) { - checkWork(); - if (member.kind() == SymbolKind.ATTRIBUTE) attributes.putIfAbsent(member.name(), member); - } - for (SymbolId classId : chain) { - checkWork(); - Map names = roleNamesByTarget.get(classId); - if (names == null) { - continue; - } - for (DeclInfo memberDecl : attributes.values()) { - checkWork(); - DeclInfo role = names.get(memberDecl.name()); - if (role == null) continue; - DeclInfo association = role.owner().flatMap(index::byId).orElse(null); - DeclInfo attributeOwner = memberDecl.owner().flatMap(index::byId).orElse(null); - // A later association cannot retroactively add an inherited name to - // an already declared subclass. The direct target still owns its - // own namespace and is checked regardless of declaration order. - if (!memberDecl.owner().filter(classId::equals).isPresent() - && association != null && attributeOwner != null - && association.sourceId().equals(attributeOwner.sourceId()) - && association.syntax().range().startOffset() > attributeOwner.syntax().range().startOffset()) continue; - { - emitRule(memberDecl, "attribute name conflicts with role name", - "ATTRIBUTE_ROLE_CONFLICT"); - } - } - } - } - } - - private void addRoleNamespace(Map> namesByTarget, - SymbolId target, RoleSymbol role) { - DeclInfo roleDecl = index.byId(role.id()).orElse(null); - if (roleDecl == null) { - return; - } - Map names = namesByTarget.computeIfAbsent(target, - ignored -> new LinkedHashMap<>()); - DeclInfo previous = names.putIfAbsent(roleDecl.name(), roleDecl); - if (previous != null && !previous.id().equals(roleDecl.id())) { - SymbolId firstTopic = scopeRoot(role.id()); - SymbolId secondTopic = scopeRoot(previous.id()); - boolean sharedBase = firstTopic != null && secondTopic != null - && !firstTopic.equals(secondTopic) - && shareBaseTopic(firstTopic, secondTopic); - SymbolId currentOwner = role.header().owner().orElseThrow(); - SymbolId previousOwner = previous.owner().orElseThrow(); - boolean sameFamily = baseChain(currentOwner).stream().anyMatch(baseChain(previousOwner)::contains); - if (!sharedBase && !sameFamily) { - emitRule(roleDecl, "role name conflicts with another role in the same namespace", - "ROLE_NAMESPACE_CONFLICT"); - } - } - } - - - private guru.interlis.model.ContextSymbol buildContext(DeclInfo decl) { - var syntax = (guru.interlis.syntax.ContextDeclarationSyntax) decl.syntax(); - if (index.byId(decl.owner().orElseThrow()).orElseThrow().kind() != SymbolKind.MODEL) - emitRule(decl, "CONTEXT must be declared at model level", "CONTEXT_SCOPE"); - if (index.children(decl.owner().orElseThrow()).stream() - .anyMatch(d -> !d.id().equals(decl.id()) && d.name().equals(decl.name()))) - emitRule(decl, "context name must be unique", "CONTEXT_NAME_UNIQUE"); - List mappings = new ArrayList<>(); - Set own = new HashSet<>(); - for (var mapping : syntax.mappings()) { - checkWork(); - var generic = bindTarget(decl, mapping.genericDomain(), Set.of(SymbolKind.DOMAIN), - ReferenceUsage.TYPE_USE, new ArrayList<>()); - List alternatives = new ArrayList<>(); - for (var name : mapping.alternatives()) bindTarget(decl, name, Set.of(SymbolKind.DOMAIN), - ReferenceUsage.TYPE_USE, new ArrayList<>()).ifPresent(alternatives::add); - if (generic.isEmpty()) continue; - if (!own.add(generic.get())) emitRule(decl, "duplicate generic mapping in context", "CONTEXT_DUPLICATE"); - if (new HashSet<>(alternatives).size() != alternatives.size()) - emitRule(decl, "duplicate concrete alternative", "CONTEXT_DUPLICATE"); - mappings.add(new guru.interlis.model.ContextSymbol.Mapping(generic.get(), alternatives, mapping.genericDomain().range())); - } - return new guru.interlis.model.ContextSymbol(header(decl), mappings); + // Charge the closure's per-source coverage work before ending the semantic run. + for (var source : closure.sources()) work.checkpoint(); + return new GraphResult(graph, rootIds, new ArrayList<>(modelIds), + diagnostics.unsupportedFeatures(), diagnostics.fragmentTypeFailures()); } - /** Bound contexts are model-local; their effects flow only along explicit import edges. */ - private void validateGenericSystem() { - for (Symbol symbol : symbols.values()) { - checkWork(); - if (symbol instanceof DomainSymbol d && d.generic() - && !(effectiveDescriptor(d.typeId()) instanceof CoordinateType)) - emitRule(index.byId(d.id()).orElseThrow(), "GENERIC requires a coordinate domain", "GENERIC_TYPE"); - if (!(symbol instanceof guru.interlis.model.ContextSymbol context)) continue; - DeclInfo decl = index.byId(context.id()).orElseThrow(); - for (var mapping : context.mappings()) { - var generic = (DomainSymbol) symbols.get(mapping.genericDomain()); - if (!generic.generic() || !(effectiveDescriptor(generic.typeId()) instanceof CoordinateType base)) { - emitRule(decl, "CONTEXT domain must be GENERIC coordinates", "CONTEXT_GENERIC"); continue; - } - for (SymbolId id : mapping.alternatives()) { - checkWork(); - var concrete = (DomainSymbol) symbols.get(id); - if (concrete.generic() || concrete.header().modifiers().contains(guru.interlis.model.Modifier.ABSTRACT) - || !(effectiveDescriptor(concrete.typeId()) instanceof CoordinateType target) - || target.axes().stream().anyMatch(a -> a.minimum().isEmpty() || a.maximum().isEmpty())) { - emitRule(decl, "CONTEXT alternative must be concrete coordinates", "CONTEXT_CONCRETE"); continue; - } - for (SymbolId parent : baseChain(concrete.id()).stream().skip(1).toList()) { - if (symbols.get(parent) instanceof DomainSymbol ancestor - && effectiveDescriptor(ancestor.typeId()) instanceof CoordinateType inherited - && !guru.interlis.model.internal.TypeCompatibility.coordinateRefines(target, inherited)) - emitRule(decl, "CONTEXT alternative widens its declared coordinate base", "CONTEXT_ALTERNATIVE_EXTENSION"); - } - if (base.dimensions() != target.dimensions() || base.multiCoordinate() != target.multiCoordinate()) - emitRule(decl, "CONTEXT target dimensions differ", "GENERIC_DIMENSIONS"); - else if (!guru.interlis.model.internal.TypeCompatibility.coordinateRefines(target, base)) - emitRule(decl, "CONTEXT target must refine the generic coordinates", "GENERIC_RANGE"); - } - } - } - Map> contexts = new LinkedHashMap<>(); - for (var model : index.models()) modelContexts(model.id(), contexts, new HashSet<>()); - for (Symbol symbol : List.copyOf(symbols.values())) { - checkWork(); - if (!(symbol instanceof TopicSymbol topic) || index.byId(topic.id()).isEmpty()) continue; - DeclInfo decl = index.byId(topic.id()).orElseThrow(); - SymbolId model = index.modelOfSymbol(topic.id()).orElseThrow(); - Set visible = importedModels(model); - Set used = new LinkedHashSet<>(); - Set viewables = new LinkedHashSet<>(); - for (SymbolId id : baseChain(topic.id())) for (DeclInfo child : index.children(id)) - if (Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION, SymbolKind.VIEW).contains(child.kind())) viewables.add(child.id()); - // EXTENDED replaces the inherited declaration in this topic's effective population. - var overridden = new HashSet(); - for (SymbolId id : viewables) if (symbols.get(id).header().modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) - overridden.addAll(baseChain(id).stream().filter(base -> !base.equals(id)).toList()); - viewables.removeAll(overridden); - Set seen = new HashSet<>(); - Map> referenceUses=new LinkedHashMap<>(); - for (SymbolId id : viewables) collectViewableGenerics(id, visible, topic.id(), used, referenceUses, seen); - var available = contexts.getOrDefault(model, Map.of()); - Set declaredDeferred = new LinkedHashSet<>(); - for (SymbolId id : baseChain(topic.id())) if (symbols.get(id) instanceof TopicSymbol t) declaredDeferred.addAll(t.deferredGenerics()); - for (SymbolId id : topic.deferredGenerics()) { - if (!(symbols.get(id) instanceof DomainSymbol domain) || !domain.generic()) - emitRule(decl, "DEFERRED GENERICS references a non-generic domain", "GENERIC_DEFERRED_UNKNOWN"); - if (!used.contains(id)) emitRule(decl, "DEFERRED GENERICS domain is unused", "GENERIC_DEFERRED_UNUSED"); - if (!available.containsKey(id)) emitRule(decl, "DEFERRED GENERICS domain has no CONTEXT", "GENERIC_CONTEXT_MISSING"); - } - List bindings = new ArrayList<>(); - for (SymbolId generic : used) { - var binding = available.get(generic); - if(binding!=null) for(var range:referenceUses.getOrDefault(generic,Set.of())) for(var alternative:binding.alternatives()) { - checkWork();var reference=range.referenceSystem().orElseThrow(); - var concrete=new guru.interlis.model.NumericRange(range.minimum(),range.maximum(),range.circular(),range.direction(),range.unit(), - Optional.of(new guru.interlis.model.NumericRange.ReferenceSystem(alternative,true,reference.axis()))); - // Validate known alternatives without rewriting the published generic identity. - metadata.validateRange(decl,concrete); - } - if (binding == null) { - if (topic.header().modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) - bindings.add(new TopicSymbol.GenericBinding(generic, List.of(), List.of(), TopicSymbol.BindingState.ABSTRACT)); - else emitRule(decl, "generic domain is used without an effective CONTEXT", "GENERIC_CONTEXT_MISSING"); - } else if (binding.alternatives().size() > 1 && !declaredDeferred.contains(generic)) { - emitRule(decl, "multiple CONTEXT alternatives require DEFERRED GENERICS", "GENERIC_DEFERRED_REQUIRED"); - } else if (!binding.alternatives().isEmpty()) bindings.add(binding); - } - symbols.put(topic.id(), new TopicSymbol(topic.header(), topic.baseTopic(), topic.members(), topic.dependencies(), - topic.deferredGenerics(), bindings)); + record GraphResult(SemanticGraph graph, List roots, List models, + Map unsupportedFeatures, + Set fragmentTypeFailures) { + GraphResult { + Objects.requireNonNull(graph, "graph"); + roots = List.copyOf(Objects.requireNonNull(roots, "roots")); + models = List.copyOf(Objects.requireNonNull(models, "models")); + unsupportedFeatures = unsupportedFeatures.isEmpty() ? Map.of() : Map.copyOf(unsupportedFeatures); + fragmentTypeFailures = Set.copyOf(fragmentTypeFailures); } } - private Map modelContexts(SymbolId model, - Map> completed, Set active) { - if (symbols.get(model) instanceof ModelSymbol predefined && predefined.origin() instanceof Origin.PredefinedOrigin) return Map.of(); - if (completed.containsKey(model)) return completed.get(model); - if (!active.add(model)) return Map.of(); // ImportBinder diagnoses cycles. - Map result = new LinkedHashMap<>(); - ModelSymbol owner = (ModelSymbol) symbols.get(model); - DeclInfo decl = index.byId(model).orElseThrow(); - for (var imported : owner.imports()) if (imported.targetModel().isPresent()) { - checkWork(); - for (var entry : modelContexts(imported.targetModel().get(), completed, active).entrySet()) { - var previous = result.get(entry.getKey()); var incoming = entry.getValue(); - if (previous == null) result.put(entry.getKey(), incoming); - else { - var candidates = new LinkedHashSet<>(previous.alternatives()); candidates.addAll(incoming.alternatives()); - candidates.removeIf(id -> previous.alternatives().stream().noneMatch(base -> domainExtends(id, base)) - || incoming.alternatives().stream().noneMatch(base -> domainExtends(id, base))); - var origins = new LinkedHashSet<>(previous.contexts()); origins.addAll(incoming.contexts()); - if (candidates.isEmpty()) emitRule(decl, "imported contexts have incompatible alternatives", "CONTEXT_CONFLICT"); - result.put(entry.getKey(), genericBinding(entry.getKey(), List.copyOf(candidates), List.copyOf(origins))); - } - } - } - for (SymbolId member : owner.members()) if (symbols.get(member) instanceof guru.interlis.model.ContextSymbol context) { - for (var mapping : context.mappings()) { - checkWork(); - var previous = result.get(mapping.genericDomain()); - if (previous != null && mapping.alternatives().stream().anyMatch(id -> - previous.alternatives().stream().noneMatch(base -> domainExtends(id, base)))) - emitRule(index.byId(context.id()).orElseThrow(), "CONTEXT target must extend an existing alternative", "GENERIC_CONTEXT_EXTENDS"); - result.put(mapping.genericDomain(), genericBinding(mapping.genericDomain(), mapping.alternatives(), List.of(context.id()))); - } - } - active.remove(model); completed.put(model, result); return result; - } - - private TopicSymbol.GenericBinding genericBinding(SymbolId generic, List alternatives, List contexts) { - // Empty alternatives can occur only in a diagnosed partial analysis, not in a valid schema. - return new TopicSymbol.GenericBinding(generic, alternatives, contexts, alternatives.isEmpty() - ? TopicSymbol.BindingState.ABSTRACT : alternatives.size() == 1 ? TopicSymbol.BindingState.CONCRETE : TopicSymbol.BindingState.DEFERRED); - } - - Set importedModels(SymbolId model) { - Set result = new LinkedHashSet<>(); var pending = new java.util.ArrayDeque(); pending.add(model); - while (!pending.isEmpty()) { - checkWork(); SymbolId next = pending.removeFirst(); - if (result.add(next) && symbols.get(next) instanceof ModelSymbol m) - m.imports().forEach(i -> i.targetModel().ifPresent(pending::add)); - } - return result; - } - - private void collectViewableGenerics(SymbolId viewable, Set visible, SymbolId topic, - Set used, Map> referenceUses, Set seen) { - for (var member : effectiveMembers.getOrDefault(viewable, List.of())) { - checkWork(); - if (symbols.get(member.declaredSymbol()) instanceof AttributeSymbol attr) collectGenericUses(attr.declaredType(), visible, topic, used, referenceUses, seen); - } - } - - private void collectGenericUses(TypeId type, Set visible, SymbolId topic, Set used, - Map> referenceUses, Set seen) { - if (type == null || !seen.add(type)) return; - checkWork(); TypeDescriptor descriptor = types.get(type); - if (descriptor instanceof DomainReferenceType ref && symbols.get(ref.targetDomain()) instanceof DomainSymbol domain) { - if (domain.generic()) used.add(domain.id()); - collectGenericUses(domain.typeId(), visible, topic, used, referenceUses, seen); - } else if (descriptor instanceof GeometryType geometry) { - geometry.vertexDomain().map(symbols::get).filter(DomainSymbol.class::isInstance).map(DomainSymbol.class::cast).ifPresent(domain -> { - if (domain.generic()) used.add(domain.id()); collectGenericUses(domain.typeId(), visible, topic, used, referenceUses, seen); - }); - } else if (descriptor instanceof NumericType numeric) { - collectRangeGenerics(numeric.range(),visible,topic,used,referenceUses,seen); - } else if (descriptor instanceof CoordinateType coordinate) { - for(var range:coordinate.axes())collectRangeGenerics(range,visible,topic,used,referenceUses,seen); - } else if (descriptor instanceof CollectionType collection) { - collectGenericUses(collection.elementType(), visible, topic, used, referenceUses, seen); - } else if (descriptor instanceof StructureValueType structure) { - for (Symbol symbol : symbols.values()) if (symbol instanceof StructureSymbol candidate - && visible.contains(index.modelOfSymbol(candidate.id()).orElse(null)) - && (structure.targetRoots().isEmpty() || structure.targetRoots().stream().anyMatch(root -> baseChain(candidate.id()).contains(root))) - && (!(symbols.get(scopeRoot(candidate.id())) instanceof TopicSymbol) || baseChain(topic).contains(scopeRoot(candidate.id())))) - collectViewableGenerics(candidate.id(), visible, topic, used, referenceUses, seen); - } - } - - private void collectRangeGenerics(guru.interlis.model.NumericRange range,Set visible,SymbolId topic, - Set used,Map> referenceUses,Set seen) { - checkWork();var reference=range.referenceSystem().orElse(null); - if(reference==null||!reference.domain())return; - if(symbols.get(reference.target()) instanceof DomainSymbol domain&&domain.generic()) { - used.add(domain.id());referenceUses.computeIfAbsent(domain.id(),ignored->new LinkedHashSet<>()).add(range); - collectGenericUses(domain.typeId(),visible,topic,used,referenceUses,seen); - } - } - - private String lastSegment(String name) { - int dot = name.lastIndexOf('.'); - return dot < 0 ? name : name.substring(dot + 1); - } - - private void buildModel(IndexedDeclarations.ModelEntry entry) { - DeclInfo decl = entry.declaration(); - guru.interlis.syntax.ModelSyntax modelSyntax = entry.model().syntax(); - try { - String issuer = modelSyntax.issuer(); - if (issuer.length() > 1023 || issuer.chars().anyMatch(c -> c > 127)) - throw new java.net.URISyntaxException(issuer, "issuer outside URI character/length profile"); - new java.net.URI(issuer); - } catch (java.net.URISyntaxException invalid) { - emitRule(decl, "model issuer must be an ASCII URI reference of at most 1023 characters", "MODEL_ISSUER"); - } - if (modelSyntax.endName().isPresent() - && !modelSyntax.endName().orElseThrow().equals(modelSyntax.name())) { - emitRule(decl, "MODEL end name does not match declaration", "MODEL_END_NAME"); - } - if (modelSyntax.modelKind() == guru.interlis.model.ModelKind.SYMBOLOGY - && modelSyntax.languageVersion() == guru.interlis.model.LanguageVersion.ILI23 && !modelSyntax.contracted()) - emitRule(decl, "SYMBOLOGY MODEL requires CONTRACTED in INTERLIS 2.3", "SYMBOLOGY_CONTRACTED"); - if (modelSyntax.modelKind() == guru.interlis.model.ModelKind.TYPE) { - for (var child : index.children(decl.id())) { - checkWork(); - if (Set.of(SymbolKind.TOPIC, SymbolKind.PARAMETER).contains(child.kind()) - || child.kind() == SymbolKind.METADATA_BASKET - && (!(child.syntax() instanceof guru.interlis.syntax.MetadataBasketDeclarationSyntax basket) - || basket.basketKind()!=guru.interlis.model.MetadataBasketSymbol.Kind.REFSYSTEM) - || child.kind() == SymbolKind.CLASS && !child.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) - emitRule(child, "TYPE MODEL may only declare type definitions", "TYPE_MODEL_CONTENT"); - } - } - // The inventory may retain several versions. Uniqueness belongs to the - // selected closure, not to every available source in the workspace. - if (closure.models().contains(entry.model())) { - DeclInfo previousModel = modelNames.putIfAbsent(modelSyntax.name(), decl); - if (previousModel != null && !previousModel.id().equals(decl.id())) { - emitRule(decl, "model name must be unique in the selected closure", "MODEL_NAME_UNIQUE"); - } - } - Set parameterNames = new LinkedHashSet<>(); - for (var child : index.children(decl.id())) { - if (child.kind() == SymbolKind.PARAMETER && !parameterNames.add(child.name())) - emitRule(child, "runtime parameter name must be unique in its model", "PARAMETER_NAME_UNIQUE"); - } - Set modelLevelNames = new LinkedHashSet<>(); - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - if (child.kind() == SymbolKind.DOMAIN || child.kind() == SymbolKind.TOPIC) { - if (!modelLevelNames.add(child.name())) { - emitRule(child, "topic/domain name must be unique in a model", - "MODEL_MEMBER_NAME_UNIQUE"); - } - } - if (child.kind() == SymbolKind.TOPIC) { - Set classNames = new LinkedHashSet<>(); - for (DeclInfo topicChild : index.children(child.id())) { - checkWork(); - if (topicChild.kind() == SymbolKind.CLASS - && !classNames.add(topicChild.name())) { - emitRule(topicChild, "class name must be unique in a topic", - "TOPIC_CLASS_NAME_UNIQUE"); - } - } - } - } - List members = index.children(decl.id()).stream().map(DeclInfo::id).toList(); - List imports = new ArrayList<>(); - for (guru.interlis.syntax.ImportSyntax site : entry.model().syntax().imports()) { - checkWork(); - SymbolId target = null; - for (Closure.ImportEdge edge : closure.importEdges()) { - checkWork(); - if (edge.importer().sourceId().equals(entry.model().sourceId()) - && edge.importer().declarationOffset() == entry.model().declarationOffset() - && edge.site().range().equals(site.range())) { - target = findModelId(edge.target().name(), edge.target().declarationOffset(), - edge.target().sourceId()).orElse(null); - break; - } - } - if (site.modelName().equals("INTERLIS")) target = PredefinedModels.ensure(this, entry.declaration().language()); - imports.add(new ModelImport(site.modelName(), site.unqualified(), site.range(), - Optional.ofNullable(target))); - } - var modelOptions = entry.model().syntax().options(); - modelOptions.characterSet().ifPresent(option -> { - if (CharacterSets.canonical(option.value()).isEmpty()) emitRule(decl, "CHARSET name is not registered by IANA", "MODEL_CHARSET", ((Origin.DeclaredOrigin)option.origin()).range()); - }); - modelOptions.xmlNamespace().ifPresent(option -> { - try { - var uri = new java.net.URI(option.value()); - if (option.value().isEmpty() || option.value().equals("http://www.w3.org/2000/xmlns/") - || option.value().equals("http://www.w3.org/XML/1998/namespace")) - throw new java.net.URISyntaxException(option.value(), "reserved or empty namespace"); - } catch (java.net.URISyntaxException invalid) { - emitRule(decl, "XMLNS must be a nonempty, unreserved namespace URI reference", "MODEL_XMLNS", ((Origin.DeclaredOrigin)option.origin()).range()); - } - }); - guru.interlis.syntax.ModelSyntax syntax = entry.model().syntax(); - Optional translation = syntax.translationSource() - .map(source -> new guru.interlis.model.ModelTranslation(source.text(), - syntax.translationVersion(), syntax.language(), Optional.empty(), List.of())); - translation=overlays.translation(decl.id(),translation); - symbols.put(decl.id(), new ModelSymbol(header(decl), syntax.modelKind(), - syntax.contracted(), entry.model().version(), syntax.issuer(), imports, members, - translation, syntax.options(), modelNamespace(syntax))); - } - - private String modelNamespace(guru.interlis.syntax.ModelSyntax syntax) { - return syntax.languageVersion() == guru.interlis.model.LanguageVersion.ILI23 ? "http://www.interlis.ch/INTERLIS2.3" - : syntax.options().xmlNamespace().map(guru.interlis.model.ModelOptions.TextValue::value) - .orElse("http://www.interlis.ch/xtf/2.4/" + syntax.name()); - } - - /** Translations share the original transfer vocabulary, independent of file/declaration order. */ - private void bindTransferNamespaces() { - for (var symbol : List.copyOf(symbols.values())) { - if (!(symbol instanceof ModelSymbol model) || model.translation().isEmpty()) continue; - Set seen = new HashSet<>(); - ModelSymbol original = model; - while (original.translation().flatMap(guru.interlis.model.ModelTranslation::sourceTarget).isPresent() - && seen.add(original.id())) { - checkWork(); - var next = symbols.get(original.translation().orElseThrow().sourceTarget().orElseThrow()); - if (!(next instanceof ModelSymbol base)) break; - original = base; - } - String namespace = original.transferNamespace(); - if (model.options().xmlNamespace().filter(value -> !value.value().equals(namespace)).isPresent()) - emitRule(index.byId(model.id()).orElseThrow(), "translated XMLNS conflicts with the original transfer namespace", "TRANSLATION_XMLNS"); - symbols.put(model.id(), new ModelSymbol(model.header(), model.modelKind(), model.contracted(), model.modelVersion(), - model.issuer(), model.imports(), model.members(), model.translation(), model.options(), namespace)); - } - } - - Symbol buildSymbol(DeclInfo decl) { - checkWork(); - Symbol existing = symbols.get(decl.id()); - if (existing != null) { - return existing; - } - Symbol symbol = switch (decl.kind()) { - case TOPIC -> buildTopic(decl); - case CLASS -> buildClass(decl); - case STRUCTURE -> buildStructure(decl); - case ASSOCIATION -> buildAssociation(decl); - case DOMAIN -> buildDomain(decl); - case UNIT -> buildUnit(decl); - case LINE_FORM -> lines.buildForm(decl); - case METADATA_BASKET -> metadata.basket(decl); - case METAOBJECT -> metadata.object(decl); - case FUNCTION -> buildFunction(decl); - case PARAMETER -> buildParameter(decl); - case CONTEXT -> buildContext(decl); - case ATTRIBUTE -> buildAttribute(decl); - case ROLE -> buildRole(decl); - case CONSTRAINT -> buildConstraint(decl); - case ENUM_ELEMENT -> buildEnumElement(decl); - case UNSUPPORTED -> buildUnsupported(decl); - case GRAPHIC -> graphics.build(decl); - case DRAWING_RULE -> { - buildSymbol(index.byId(decl.owner().orElseThrow()).orElseThrow()); - yield symbols.get(decl.id()); - } - case VIEW -> buildView(decl); - case VIEW_BASE -> views.buildBase(decl); - case MODEL -> throw new IllegalStateException("model symbols are built separately"); - }; - symbols.put(decl.id(), symbol); - return symbol; - } - - private void validateTopicInheritanceCycles() { - for (Symbol symbol : symbols.values()) { - checkWork(); - if (symbol instanceof TopicSymbol && hasCycle(baseChain(symbol.id()))) - index.byId(symbol.id()).ifPresent(this::emitCycle); - } - } - - private TopicSymbol buildTopic(DeclInfo decl) { - TopicSyntax syntax = (TopicSyntax) decl.syntax(); - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - buildSymbol(child); - } - if (syntax.endName().isPresent() && !syntax.endName().orElseThrow().equals(syntax.name())) { - emitRule(decl, "TOPIC end name does not match declaration", "TOPIC_END_NAME"); - } - Optional base = syntax.baseTopic().flatMap(name -> { - Optional resolved = tryResolve(decl, name, Set.of(SymbolKind.TOPIC)); - if (resolved.isEmpty()) { - List matches = symbols.values().stream() - .filter(TopicSymbol.class::isInstance).map(TopicSymbol.class::cast) - .filter(topic -> topic.name().value().equals(name.text())) - .map(TopicSymbol::id).toList(); - if (matches.size() == 1) { - return Optional.of(matches.getFirst()); - } - } - return resolved; - }); - base.ifPresent(id -> baseOf.put(decl.id(), id)); - if (base.filter(this::isFinalDeclaration).isPresent()) - emitRule(decl, "cannot extend FINAL topic", "TOPIC_FINAL"); - if (base.isPresent() && symbols.get(base.orElseThrow()) instanceof TopicSymbol baseTopic) { - boolean abstractBase = baseTopic.header().modifiers() - .contains(guru.interlis.model.Modifier.ABSTRACT); - for (SymbolId member : baseTopic.members()) { - checkWork(); - if (symbols.get(member) instanceof ClassSymbol baseClass - && baseClass.header().modifiers() - .contains(guru.interlis.model.Modifier.ABSTRACT)) { - abstractBase = true; - } - } - Set baseDomains = new LinkedHashSet<>(); - for (SymbolId member : baseTopic.members()) { - checkWork(); - if (symbols.get(member) instanceof DomainSymbol domain) { - baseDomains.add(domain.name().value()); - } - } - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - if (child.kind() == SymbolKind.DOMAIN && baseDomains.contains(child.name())) { - emitRule(child, "domain name conflicts with an inherited topic member", - "INHERITED_NAME_CONFLICT"); - } - } - boolean baseHasConcrete = base.flatMap(index::byId).map(baseDecl -> { - for (DeclInfo child : index.children(baseDecl.id())) { - checkWork(); - if (child.kind() == SymbolKind.STRUCTURE) { - return true; - } - if (child.kind() == SymbolKind.CLASS - && !child.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) { - return true; - } - } - return false; - }).orElse(false); - boolean derivedHasContent = !index.children(decl.id()).isEmpty() - || (decl.syntax() instanceof guru.interlis.syntax.TopicSyntax topicSyntax - && !topicSyntax.oidRules().isEmpty()) - || !syntax.deferredGenerics().isEmpty(); - if (abstractBase && !derivedHasContent && !baseHasConcrete && !syntax.modifiers() - .contains(guru.interlis.model.Modifier.ABSTRACT)) { - emitRule(decl, "empty topic extending an abstract topic must be abstract", - "ABSTRACT_TOPIC_EXTENSION"); - } - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - if (child.kind() != SymbolKind.CLASS - || !(symbols.get(child.id()) instanceof ClassSymbol derivedClass) - || derivedClass.baseType().isEmpty()) { - continue; - } - for (SymbolId member : baseTopic.members()) { - checkWork(); - if (!(symbols.get(member) instanceof ClassSymbol baseClass) - || !baseClass.name().equals(derivedClass.name())) { - continue; - } - if (!child.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - emitRule(child, "class in an extending topic may not extend the " - + "same-named base class without EXTENDED", "TOPIC_CLASS_EXTENSION"); - } - if (baseClass.header().modifiers() - .contains(guru.interlis.model.Modifier.EXTENDED)) { - emitRule(child, "cannot extend a class marked EXTENDED", - "EXTENDED_BASE_CLASS"); - } - } - } - } - List dependencies = new ArrayList<>(); - for (guru.interlis.syntax.NameSyntax dependency : syntax.dependencies()) { - checkWork(); - Optional resolved = resolver.resolve(decl.id(), dependency, Set.of(SymbolKind.TOPIC)).target(); - if (resolved.isEmpty()) unresolved(dependency); - resolved.ifPresent(dependencies::add); - recordReference(decl, dependency, resolved.orElse(null), ReferenceUsage.TYPE_USE); - } - Set memberNames = new LinkedHashSet<>(); - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - if (child.kind() == SymbolKind.CLASS || child.kind() == SymbolKind.DOMAIN) { - if (!memberNames.add(child.name())) { - emitRule(child, "name conflict in topic", "TOPIC_MEMBER_NAME"); - } - } - } - if (!syntax.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) { - List classes = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.CLASS).toList(); - for (DeclInfo child : classes) { - checkWork(); - if (child.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT) - && classes.stream().noneMatch(candidate -> - !candidate.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT) - && baseChain(candidate.id()).contains(child.id()))) - emitRule(decl, "abstract class requires an abstract topic or a concrete specialization", - "ABSTRACT_TOPIC"); - } - } - List members = index.children(decl.id()).stream().map(DeclInfo::id).toList(); - List deferred = new ArrayList<>(); - for (var name : syntax.deferredGenerics()) bindTarget(decl, name, Set.of(SymbolKind.DOMAIN), - ReferenceUsage.TYPE_USE, new ArrayList<>()).ifPresent(deferred::add); - return new TopicSymbol(header(decl), base, members, dependencies, deferred, List.of()); - } - - private ClassSymbol buildClass(DeclInfo decl) { - ClassDeclarationSyntax syntax = (ClassDeclarationSyntax) decl.syntax(); - if (syntax.endName().isPresent() && !syntax.endName().orElseThrow().equals(syntax.name())) { - emitRule(decl, "CLASS end name does not match declaration", "CLASS_END_NAME"); - } - Optional base = objectInheritanceBase(decl.id()); - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - buildSymbol(child); - } - List members = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.ATTRIBUTE || child.kind() == SymbolKind.ROLE || child.kind() == SymbolKind.PARAMETER) - .map(DeclInfo::id).toList(); - List constraints = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.CONSTRAINT) - .map(DeclInfo::id).toList(); - for (SymbolId member : members) { - checkWork(); - DeclInfo memberDecl = index.byId(member).orElse(null); - if (memberDecl != null && memberDecl.kind() == SymbolKind.ATTRIBUTE) { - TypeId typeId = declaredTypes.get(member); - if (decl.language() != guru.interlis.model.LanguageVersion.ILI24 - && typeId != null && types.get(typeId) instanceof ReferenceType) { - emitRule(memberDecl, "reference attribute is only allowed in a structure", - "REFERENCE_ATTRIBUTE_CLASS"); - } - } - } - return new ClassSymbol(header(decl), base, members, constraints); - } - - private StructureSymbol buildStructure(DeclInfo decl) { - StructureDeclarationSyntax syntax = (StructureDeclarationSyntax) decl.syntax(); - Optional base = objectInheritanceBase(decl.id()); - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - buildSymbol(child); - } - List members = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.ATTRIBUTE || child.kind() == SymbolKind.ROLE || child.kind() == SymbolKind.PARAMETER) - .map(DeclInfo::id).toList(); - List constraints = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.CONSTRAINT) - .map(DeclInfo::id).toList(); - return new StructureSymbol(header(decl), base, members, constraints); - } - - /** Bind inheritance before members, including members reached from another source. */ - Optional objectInheritanceBase(SymbolId owner) { - DeclInfo decl = index.byId(owner).orElse(null); - if (decl == null || (decl.kind() != SymbolKind.CLASS && decl.kind() != SymbolKind.STRUCTURE) - || !resolvedObjectBases.add(owner)) return Optional.ofNullable(baseOf.get(owner)); - Optional name = decl.syntax() instanceof ClassDeclarationSyntax c - ? c.baseType() : ((StructureDeclarationSyntax) decl.syntax()).baseType(); - if (name.isPresent() && decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) - emitRule(decl, "EXTENDED and EXTENDS cannot be combined in one declaration", "EXTENDED_EXPLICIT_BASE"); - Set kinds = decl.kind() == SymbolKind.CLASS - ? Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE) : Set.of(SymbolKind.STRUCTURE); - Optional base=name.flatMap(n->bindTarget(decl,n,kinds,ReferenceUsage.BASE_TYPE,new ArrayList<>())); - - if (base.isEmpty() && decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - base = baseTopicMember(owner, decl.name(), Set.of(decl.kind())); - if (base.isEmpty()) emitRule(decl, "EXTENDED needs a same-named inherited declaration", "EXTENDED_BASE"); - } - base.ifPresent(id -> { - baseOf.put(owner, id); - if (isFinalDeclaration(id)) - emitRule(decl, "cannot extend FINAL object type", "OBJECT_FINAL"); - }); - return base; - } - - /** Declaration metadata is available before an imported base is built. */ - private boolean isFinalDeclaration(SymbolId id) { - return index.byId(id).map(info -> info.modifiers().contains(guru.interlis.model.Modifier.FINAL)) - .orElseGet(() -> symbols.containsKey(id) && symbols.get(id).header().has(guru.interlis.model.Modifier.FINAL)); - } - - private AssociationSymbol buildAssociation(DeclInfo decl) { - AssociationDeclarationSyntax syntax = (AssociationDeclarationSyntax) decl.syntax(); - buildingAssociations.add(decl.id()); - Optional base = syntax.baseAssociation().flatMap(name -> { - var resolved = resolver.resolve(decl.id(), name, Set.of(SymbolKind.ASSOCIATION)).target(); - if (resolved.isEmpty()) unresolved(name); - return resolved; - }); - if (base.isEmpty() && syntax.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - base = baseTopicMember(decl.id(), decl.name(), Set.of(SymbolKind.ASSOCIATION)); - if (base.isEmpty()) emitRule(decl, "EXTENDED association needs an inherited base", "ASSOCIATION_BASE"); - } - if (base.isPresent()) { - SymbolId parent = base.orElseThrow(); - if (buildingAssociations.contains(parent)) { - emitRule(decl, "cyclic association inheritance", "INHERITANCE_CYCLE"); - base = Optional.empty(); - } else { - buildSymbol(index.byId(parent).orElseThrow()); - baseOf.put(decl.id(), parent); - if (symbols.get(parent).header().modifiers().contains(guru.interlis.model.Modifier.FINAL)) { - emitRule(decl, "cannot extend FINAL association", "ASSOCIATION_FINAL"); - } - } - } - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - buildSymbol(child); - } - buildingAssociations.remove(decl.id()); - if (syntax.nameRange().isPresent() != syntax.endName().isPresent()) { - emitRule(decl, "association name must be present at both begin and END", "ASSOCIATION_NAME"); - } - Set roleNames = new LinkedHashSet<>(); - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - if (child.kind() == SymbolKind.ROLE && !roleNames.add(child.name())) { - emitRule(child, "duplicate role name", "DUPLICATE_ROLE"); - } - } - List roles = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.ROLE) - .map(DeclInfo::id).toList(); - List attributes = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.ATTRIBUTE) - .map(DeclInfo::id).toList(); - if (roles.isEmpty() && attributes.isEmpty() && base.isEmpty()) { - emitRule(decl, "association requires at least two roles", "ASSOCIATION_ROLES"); - } - List constraints = index.children(decl.id()).stream() - .filter(child -> child.kind() == SymbolKind.CONSTRAINT) - .map(DeclInfo::id).toList(); - Cardinality inheritedCardinality = base.map(symbols::get).map(AssociationSymbol.class::cast) - .map(AssociationSymbol::cardinality).orElse(Cardinality.exactly(1)); - Cardinality effectiveCardinality = syntax.cardinality().orElse(inheritedCardinality); - if (base.isPresent() && (effectiveCardinality.lower().compareTo(inheritedCardinality.lower()) < 0 - || widerUpper(effectiveCardinality.upper(), inheritedCardinality.upper()))) { - emitRule(decl, "association extension widens link cardinality", "ASSOCIATION_CARDINALITY_EXTENSION"); - } - return new AssociationSymbol(header(decl), base, effectiveCardinality, roles, attributes, constraints, - index.children(decl.id()).stream().filter(d -> d.kind() == SymbolKind.VIEW_BASE).map(DeclInfo::id).findFirst()); - } - - private guru.interlis.model.ParameterSymbol buildParameter(DeclInfo decl) { - var syntax = (guru.interlis.syntax.ParameterDeclarationSyntax) decl.syntax(); - var model = index.modelOfSymbol(decl.id()).flatMap(index::byId) - .map(m -> (guru.interlis.syntax.ModelSyntax) m.syntax()).orElseThrow(); - if (index.byId(decl.owner().orElseThrow()).orElseThrow().kind() == SymbolKind.MODEL - && decl.language() == guru.interlis.model.LanguageVersion.ILI23 && !model.contracted()) - emitRule(decl, "runtime parameters require CONTRACTED in INTERLIS 2.3", "PARAMETER_CONTRACTED"); - TypeId type = buildType(syntax.type(), decl.id(), decl.sourceId()); - if (index.byId(decl.owner().orElseThrow()).orElseThrow().kind() == SymbolKind.MODEL && containsArea(type, new HashSet<>())) - emitRule(decl, "runtime parameters cannot contain AREA values", "PARAMETER_AREA"); - declaredTypes.put(decl.id(), type); - return new guru.interlis.model.ParameterSymbol(header(decl), type, syntax.mandatory() || mandatoryType(type), Optional.empty()); - } - - private boolean containsArea(TypeId type, Set seen) { - if (!seen.add(type)) return false; - TypeDescriptor descriptor = effectiveDescriptor(type); - if (descriptor instanceof GeometryType geometry) - return geometry.geometryKind() == guru.interlis.model.GeometryKind.AREA - || geometry.geometryKind() == guru.interlis.model.GeometryKind.MULTIAREA; - if (descriptor instanceof CollectionType collection) return containsArea(collection.elementType(), seen); - if (descriptor instanceof StructureValueType structure) { - for(SymbolId root:structure.targetRoots()) { - Set visited=new HashSet<>(); - for(SymbolId current=root;current!=null && visited.add(current);current=objectInheritanceBase(current).orElse(null)) { - checkWork(); - if(symbols.get(current) instanceof StructureSymbol known) { - for(SymbolId member:known.members()) - if(symbols.get(member) instanceof AttributeSymbol attribute && containsArea(attribute.declaredType(),seen))return true; - } else for(var child:index.children(current)) - if(child.kind()==SymbolKind.ATTRIBUTE && containsArea(attributeType(child),seen))return true; - } - } - } - return false; - } - - private FunctionSymbol buildFunction(DeclInfo decl) { - FunctionDeclarationSyntax syntax = (FunctionDeclarationSyntax) decl.syntax(); - var model = index.modelOfSymbol(decl.id()).flatMap(index::byId) - .map(m -> (guru.interlis.syntax.ModelSyntax) m.syntax()); - if (decl.language() == guru.interlis.model.LanguageVersion.ILI23 - && model.isPresent() && !model.orElseThrow().contracted()) { - emitRule(decl, "functions require CONTRACTED in INTERLIS 2.3", "FUNCTION_CONTRACTED"); - } - Set names = new LinkedHashSet<>(); - for (var parameter : syntax.parameters()) { - checkWork(); - if (!names.add(parameter.name())) { - emitRule(decl, "duplicate function parameter", "FUNCTION_PARAMETER_DUPLICATE"); - } - } - List parameters = java.util.stream.IntStream.range(0, syntax.parameters().size()) - .mapToObj(i -> new FunctionParameter(syntax.parameters().get(i).name(), functionArgumentType(decl, i), syntax.parameters().get(i).mandatory() || mandatoryType(functionArgumentType(decl,i)))).toList(); - TypeId result=buildType(syntax.returnType(), decl.id(), decl.sourceId()); - return new FunctionSymbol(header(decl), parameters,result,syntax.returnMandatory() || mandatoryType(result),syntax.explanation().map(e->new Documentation(e.text(),List.of(e.range()))).orElseGet(Documentation::empty)); - } - - private final Map functionArgumentTypes = new LinkedHashMap<>(); - private final Set resolvingFunctionArguments = new HashSet<>(); - - private TypeId functionArgumentType(DeclInfo function, int position) { - var key = new guru.interlis.model.AttributePathType.Argument(function.id(), position); - TypeId bound = functionArgumentTypes.get(key); - if (bound != null) return bound; - if (!resolvingFunctionArguments.add(key)) { - TypeId error = nextTypeId(); - types.put(error, new ErrorType(error, function.origin(), List.of(emitRule(function, - "cyclic attribute-path argument dependency", "FUNCTION_ARGUMENT_CYCLE")))); - return error; - } - var syntax = (FunctionDeclarationSyntax) function.syntax(); - TypeId type = buildType(syntax.parameters().get(position).type(), function.id(), function.sourceId()); - functionArgumentTypes.put(key, type); resolvingFunctionArguments.remove(key); - return type; - } - - private UnitSymbol buildUnit(DeclInfo decl) { - UnitDeclarationSyntax syntax = (UnitDeclarationSyntax) decl.syntax(); - Optional base = syntax.base().flatMap(name -> bindUnit(decl, name)); - Optional derived = syntax.derivedFrom().flatMap(name -> bindUnit(decl, name)); - List components = new ArrayList<>(); - for (var component : syntax.components()) { - checkWork(); - bindUnit(decl, component.unit()).ifPresent(id -> components.add(new UnitSymbol.Component(component.operator(), id))); - } - return new UnitSymbol(header(decl), syntax.designation(), syntax.abstractUnit(), base, derived, - components, syntax.conversionFactors(), syntax.conversionFunction(), syntax.definition().isBlank() ? Optional.empty() : Optional.of(syntax.definition())); - } - - Optional bindUnit(DeclInfo decl, guru.interlis.syntax.NameSyntax name) { - return bindUnit(decl,name,new ArrayList<>()); - } - - Optional bindUnit(DeclInfo decl, guru.interlis.syntax.NameSyntax name, List errors) { - return bindTarget(decl,name,Set.of(SymbolKind.UNIT),ReferenceUsage.TYPE_USE,errors,false); - } - - Optional builtinUnit(SymbolId context, String name) { - if (!name.startsWith("INTERLIS.")) return Optional.empty(); - var timeUnit = temporal.find(index.byId(context).orElseThrow(), name, Set.of(SymbolKind.UNIT)); - if (timeUnit.isPresent()) return timeUnit; - String shortName = name.substring("INTERLIS.".length()); - String parent = switch (shortName) { - case "ANYUNIT" -> ""; - case "DIMENSIONLESS", "LENGTH", "MASS", "TIME", "ELECTRIC_CURRENT", "TEMPERATURE", - "AMOUNT_OF_MATTER", "ANGLE", "SOLID_ANGLE", "LUMINOUS_INTENSITY", "MONEY" -> "ANYUNIT"; - case "m" -> "LENGTH"; case "kg" -> "MASS"; case "s" -> "TIME"; - case "A" -> "ELECTRIC_CURRENT"; case "K" -> "TEMPERATURE"; case "mol" -> "AMOUNT_OF_MATTER"; - case "rad" -> "ANGLE"; case "sr" -> "SOLID_ANGLE"; case "cd" -> "LUMINOUS_INTENSITY"; - default -> null; - }; - if (parent == null) return Optional.empty(); - var language = index.byId(context).orElseThrow().language(); - SymbolId id = new SymbolId("predefined:" + language + ":unit:" + name); - if (symbols.containsKey(id)) return Optional.of(id); - Optional base = parent.isEmpty() ? Optional.empty() : builtinUnit(context, "INTERLIS." + parent); - boolean abstractUnit = parent.isEmpty() || parent.equals("ANYUNIT"); - String designation = switch (shortName) { - case "m" -> "METER"; case "kg" -> language==guru.interlis.model.LanguageVersion.ILI23?"KILOGRAMM":"KILOGRAM"; case "s" -> "SECOND"; - case "A" -> "AMPERE"; case "K" -> "DEGREE_KELVIN"; case "mol" -> "MOLE"; - case "rad" -> "RADIAN"; case "sr" -> "STERADIAN"; case "cd" -> "CANDELA"; - default -> shortName; - }; - Origin origin = new Origin.PredefinedOrigin(language, name); - symbols.put(id, new UnitSymbol(new SymbolHeader(id, SymbolKind.UNIT, DeclaredName.synthetic(name), - Optional.empty(), 0, origin, Optional.empty(), language, - Documentation.empty(), MetaAttributeSet.empty(), abstractUnit - ? Set.of(guru.interlis.model.Modifier.ABSTRACT) : Set.of(), List.of()), - designation, abstractUnit, base, Optional.empty(), List.of(), List.of(), Optional.empty(), Optional.empty())); - return Optional.of(id); - } - - /** Validate bound dependencies after all declarations exist; file order is immaterial. */ - private void validateUnits() { - for (Symbol symbol : symbols.values()) { - if (!(symbol instanceof UnitSymbol unit)) continue; - DeclInfo decl = index.byId(unit.id()).orElse(null); - if (decl == null) continue; - checkWork(); - var syntax = (UnitDeclarationSyntax) decl.syntax(); - for (var factor : unit.conversionFactors()) { - checkWork(); - if (factor.decimal().filter(value -> value.exactValue().signum() == 0).isPresent()) { - boolean divide = factor.operator() == UnitSymbol.Operator.DIVIDE; - emitRule(decl, divide ? "unit conversion divides by zero" : "unit conversion multiplies by zero", - divide ? "UNIT_CONVERSION_ZERO_DIVISOR" : "UNIT_CONVERSION_ZERO_FACTOR", factor.range()); - } - } - if (index.children(decl.owner().orElseThrow()).stream() - .anyMatch(other -> !other.id().equals(decl.id()) && other.name().equals(decl.name()))) - emitRule(decl, "unit short name is not unique in its scope", "UNIT_NAME_UNIQUE"); - var pending = new java.util.ArrayDeque(unitDependencies(unit)); - Set seen = new HashSet<>(); - while (!pending.isEmpty()) { - checkWork(); - SymbolId next = pending.removeFirst(); - if (next.equals(unit.id())) { - emitRule(decl, "cyclic unit dependency", "UNIT_CYCLE"); break; - } - if (seen.add(next) && symbols.get(next) instanceof UnitSymbol dependency) - pending.addAll(unitDependencies(dependency)); - } - for (var component : unit.components()) { - UnitSymbol target = (UnitSymbol) symbols.get(component.unit()); - if (target.abstractUnit() != unit.abstractUnit()) - emitRule(decl, "composed unit and all components must agree on abstraction", "UNIT_COMPONENT_ABSTRACT"); - } - if (unit.derivedFrom().isPresent()) { - UnitSymbol target = (UnitSymbol) symbols.get(unit.derivedFrom().orElseThrow()); - if (unit.abstractUnit() || target.abstractUnit()) - emitRule(decl, "derived units require a concrete conversion target and are concrete", "UNIT_DERIVED_ABSTRACT"); - } - UnitSymbol base = unit.base().map(symbols::get).filter(UnitSymbol.class::isInstance).map(UnitSymbol.class::cast).orElse(null); - if (base == null) continue; - if (!base.abstractUnit()) emitRule(decl, "only abstract units can be extended", "UNIT_BASE_ABSTRACT"); - if (!syntax.components().isEmpty()) { - boolean compatible = !base.components().isEmpty() && base.components().size() == unit.components().size(); - if (compatible) for (int i = 0; i < unit.components().size(); i++) { - var actual = unit.components().get(i); var expected = base.components().get(i); - if (actual.operator() != expected.operator() || !unitExtends(actual.unit(), expected.unit())) compatible = false; - } - if (!compatible) emitRule(decl, "composition must specialize the base component by component", "UNIT_COMPOSITION_EXTENSION"); - } else if (unit.derivedFrom().isPresent() && !unitExtends(unit.derivedFrom().orElseThrow(), base.id())) { - emitRule(decl, "conversion target must specialize the declared abstract base", "UNIT_DERIVED_EXTENSION"); - } - } - } - - private List unitDependencies(UnitSymbol unit) { - List result = new ArrayList<>(); - unit.base().ifPresent(result::add); unit.derivedFrom().ifPresent(result::add); - unit.components().forEach(c -> result.add(c.unit())); return result; - } - - boolean unitExtends(SymbolId actual, SymbolId expected) { - UnitSymbol target = extensionUnit(expected); - if (target == null) return false; - if (target.name().value().equals("INTERLIS.ANYUNIT")) return true; - var pending = new java.util.ArrayDeque(); pending.add(actual); - Set seen = new HashSet<>(); - while (!pending.isEmpty()) { - checkWork(); SymbolId next = pending.removeFirst(); - if (next.equals(expected)) return true; - if (!seen.add(next)) continue; - UnitSymbol unit = extensionUnit(next); - if (unit == null) continue; - if (unit.header().origin() instanceof Origin.SyntheticOrigin - && target.header().origin() instanceof Origin.SyntheticOrigin && unit.name().equals(target.name())) return true; - unit.base().ifPresent(pending::add); unit.derivedFrom().ifPresent(pending::add); - } - return false; - } - - boolean unitsConvertible(SymbolId actual, SymbolId expected) { - return guru.interlis.model.internal.TypeCompatibility.unitRelation(actual, expected, this::extensionUnit) - .filter(relation -> relation.decision() == guru.interlis.model.RelationDecision.ALLOWED).isPresent(); - } - - private DomainSymbol buildDomain(DeclInfo decl) { - TypeId type = domainType(decl); - Set constraintNames = new LinkedHashSet<>(); - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - if (child.kind() == SymbolKind.CONSTRAINT && !constraintNames.add(child.name())) { - emitRule(child, "domain constraint names must be unique", "DOMAIN_CONSTRAINT_NAME"); - } - buildSymbol(child); - } - return new DomainSymbol(header(decl), type, Optional.ofNullable(baseOf.get(decl.id())), - decl.modifiers().contains(guru.interlis.model.Modifier.GENERIC)); - } - - /** Bind referenced types before expressions without recursively building their constraints. */ - private TypeId domainType(DeclInfo decl) { - TypeId existing = declaredTypes.get(decl.id()); - if (existing != null) return existing; - checkWork(); - if (!resolvingDomainTypes.add(decl.id())) { - TypeId error = nextTypeId(); - types.put(error, new ErrorType(error, decl.origin(), - List.of(emitRule(decl, "cyclic domain type dependency", "DOMAIN_TYPE_CYCLE")))); - declaredTypes.put(decl.id(), error); - return error; - } - try { - DomainDeclarationSyntax syntax = (DomainDeclarationSyntax) decl.syntax(); - Optional base=syntax.baseDomain().flatMap(name->bindTarget(decl,name,Set.of(SymbolKind.DOMAIN),ReferenceUsage.BASE_TYPE,new ArrayList<>())); - - base.ifPresent(id -> { - baseOf.put(decl.id(), id); - index.byId(id).ifPresent(this::domainType); - if ((symbols.get(id) != null && symbols.get(id).header().has(guru.interlis.model.Modifier.FINAL)) - || index.byId(id).filter(d -> d.modifiers().contains(guru.interlis.model.Modifier.FINAL)).isPresent()) - emitRule(decl, "cannot extend FINAL domain", "DOMAIN_FINAL"); - }); - TypeId type; - if (syntax.type() instanceof GenericTypeSyntax generic && "INHERITED".equals(generic.name()) && base.isPresent()) { - type = nextTypeId(); - types.put(type, new DomainReferenceType(type, new Origin.DeclaredOrigin(syntax.type().range()), base.orElseThrow())); - } else type = buildType(syntax.type(), decl.id(), decl.sourceId()); - TypeDescriptor inherited = base.map(declaredTypes::get).map(this::effectiveDescriptor).orElse(null); - inheritTypeProperties(type, syntax.type(), inherited,decl); - if (syntax.type() instanceof LineTypeSyntax) lines.effective(type, inherited, decl); - else if (inherited instanceof GeometryType && !(effectiveDescriptor(type) instanceof GeometryType)) - emitRule(decl, "geometry base requires a geometric extension", "GEOMETRY_EXTENSION"); - if (effectiveDescriptor(type) instanceof NumericType numeric && inherited instanceof NumericType parent) { - var relation = numericExtension(numeric, parent, decl.language()); - if (relation.decision() == guru.interlis.model.RelationDecision.DISALLOWED) - emitRule(decl, "numeric type does not specialize its base: " + relation.reason(), relation.reason().startsWith("numeric-unit") || relation.reason().contains("unit-") ? "NUMERIC_UNIT_EXTENSION" : "NUMERIC_EXTENSION"); - } - declaredTypes.putIfAbsent(decl.id(), type); - if (syntax.type() instanceof EnumerationTypeSyntax enumeration) { - if (base.isPresent() && inherited == null) emitRule(decl, "enumeration base must already be defined", "ENUM_BASE"); - types.put(type, effectiveEnumeration(type, enumeration, inherited, decl)); - } - TypeDescriptor own=effectiveDescriptor(type); - if(inherited!=null && !(inherited instanceof NumericType && own instanceof NumericType) - && !(own instanceof GeometryType) && !(inherited instanceof guru.interlis.model.OidType) - && !(own instanceof ErrorType) && !(inherited instanceof ErrorType)) { - var relation=extension(own,inherited,decl.language()); - if(relation.decision()==guru.interlis.model.RelationDecision.DISALLOWED) - emitRule(decl,"domain does not specialize its base: "+relation.reason(),"TYPE_EXTENSION"); - } - if (base.isPresent() && effectiveDescriptor(declaredTypes.get(base.orElseThrow())) instanceof guru.interlis.model.OidType oid - && (Optional.ofNullable(symbols.get(base.orElseThrow())) - .filter(s -> s.name().value().equals("INTERLIS.NOOID")).isPresent() - || oid.oidKind() != guru.interlis.model.OidKind.ANY - || !(effectiveDescriptor(type) instanceof guru.interlis.model.OidType derived) - || derived.oidKind() == guru.interlis.model.OidKind.ANY)) - emitRule(decl, "concrete OID domain cannot be extended", "OID_EXTENSION"); - return declaredTypes.get(decl.id()); - } finally { - resolvingDomainTypes.remove(decl.id()); - } - } - - private EnumerationType effectiveEnumeration(TypeId id, EnumerationTypeSyntax syntax, - TypeDescriptor inherited, DeclInfo declaration) { - EnumerationType base = inherited instanceof EnumerationType e ? e : null; - if (inherited != null && (base == null || base.treeValues())) emitRule(declaration, "enumeration definition requires an enumeration definition as base", "ENUM_BASE"); - if (base != null && base.id().equals(id)) emitRule(declaration, "cyclic enumeration inheritance", "ENUM_BASE"); - if (base != null && base.circular()) emitRule(declaration, "circular enumeration cannot be extended", "ENUM_CIRCULAR"); - if (base != null && syntax.circular()) emitRule(declaration, "enumeration extension cannot introduce circular ordering", "ENUM_ORDER"); - Map values = new LinkedHashMap<>(); - if (base != null) base.values().forEach(v -> values.put(v.path(), v)); - Set own = new LinkedHashSet<>(); - Set finals = new LinkedHashSet<>(base == null ? Set.of() : base.finalSubtrees()); - for (var value : syntax.values()) { - checkWork(); - String path = value.path(); - String parent = path.contains(".") ? path.substring(0, path.lastIndexOf('.')) : ""; - if (value.qualified() && (base == null || base.values().stream().noneMatch(v -> v.path().equals(path)))) - emitRule(declaration, "qualified enum name must identify an inherited element", "ENUM_PARENT"); - if (!own.add(path)) emitRule(declaration, "duplicate enumeration value " + path, "ENUM_VALUE_UNIQUE"); - if (values.containsKey(path)) { - if (!value.hasChildren()) emitRule(declaration, "extended enumeration repeats inherited value " + path, "ENUM_VALUE_UNIQUE"); - } else { - if (!parent.isEmpty() && !values.containsKey(parent)) emitRule(declaration, "unknown enumeration parent " + parent, "ENUM_PARENT"); - if (finals.contains(parent)) emitRule(declaration, "enumeration sibling list is FINAL: " + parent, "ENUM_FINAL"); - values.put(path, new EnumerationValue(path, 0, Optional.of(value.range()))); - } - } - finals.addAll(syntax.finalSubtrees()); - // Preserve inherited sibling order and place new descendants under their parent. - List ordered = new ArrayList<>(); - appendEnumerationChildren("", values, ordered); - return new EnumerationType(id, new Origin.DeclaredOrigin(syntax.range()), ordered, - syntax.ordered() || syntax.circular() || base != null && base.ordered(), syntax.circular(), finals, Optional.ofNullable(base).map(EnumerationType::id), false); - } - - private void appendEnumerationChildren(String parent, Map values, List result) { - for (var value : values.values()) { - checkWork(); - String path = value.path(); - String candidate = path.contains(".") ? path.substring(0, path.lastIndexOf('.')) : ""; - if (candidate.equals(parent)) { - result.add(new EnumerationValue(path, result.size(), value.range())); - appendEnumerationChildren(path, values, result); - } - } - } - - private UnitSymbol extensionUnit(SymbolId id) { - checkWork(); - Symbol symbol = symbols.get(id); - if (symbol == null) symbol = index.byId(id).map(this::buildSymbol).orElse(null); - return symbol instanceof UnitSymbol unit ? unit : null; - } - - private guru.interlis.model.TypeRelation numericExtension(NumericType actual, NumericType base, guru.interlis.model.LanguageVersion language) { - return guru.interlis.model.internal.TypeCompatibility.numericExtension(actual, base, language, this::extensionUnit); - } - - private List numericErrors(NumericTypeSyntax syntax, DeclInfo declaration) { - List errors = new ArrayList<>(); - if (syntax.minimum().isPresent() && syntax.maximum().isPresent()) { - var minimum = syntax.minimum().orElseThrow(); var maximum = syntax.maximum().orElseThrow(); - if (minimum.exactValue().compareTo(maximum.exactValue()) > 0) - errors.add(emitRule(declaration, "numeric minimum exceeds maximum", "NUMERIC_BOUNDS", syntax.range())); - if (minimum.precision().fractionalDigits() != maximum.precision().fractionalDigits()) - errors.add(emitRule(declaration, "numeric bounds must have the same precision", "NUMERIC_PRECISION", syntax.range())); - } - return List.copyOf(errors); - } - - private void validateNumericUnit(NumericRange range,DeclInfo declaration,SourceRange sourceRange,List errors) { - if(range.minimum().isPresent() && range.maximum().isPresent() && range.unit().isPresent()) { - UnitSymbol unit=extensionUnit(range.unit().orElseThrow()); - if(unit!=null && unit.abstractUnit())errors.add(emitRule(declaration,"concrete numeric range requires a concrete unit","NUMERIC_ABSTRACT_UNIT",sourceRange)); - } - } - - private void validateNumericUses() { - for(var entry:declaredTypes.entrySet()) { - checkWork();var declaration=index.byId(entry.getKey()).orElse(null); - if(declaration==null || declaration.kind()!=SymbolKind.DOMAIN && declaration.kind()!=SymbolKind.ATTRIBUTE)continue; - var type=effectiveDescriptor(entry.getValue()); - while(type instanceof CollectionType collection)type=effectiveDescriptor(collection.elementType()); - boolean incomplete=type instanceof NumericType number && (number.range().minimum().isEmpty() || number.range().maximum().isEmpty()) - || type instanceof CoordinateType coordinate && coordinate.axes().stream().anyMatch(axis->axis.minimum().isEmpty() || axis.maximum().isEmpty()); - if(!incomplete || declaration.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT))continue; - if(declaration.syntax() instanceof AttributeDeclarationSyntax attribute && attribute.type() instanceof GenericTypeSyntax inferred && inferred.name().equals("INFERRED"))continue; - boolean generic=declaration.language()==guru.interlis.model.LanguageVersion.ILI24 && type instanceof CoordinateType - && (declaration.modifiers().contains(guru.interlis.model.Modifier.GENERIC) || genericCoordinateType(entry.getValue(),new HashSet<>())); - if(!generic)emitRule(declaration,"incomplete numeric or coordinate declaration requires ABSTRACT","NUMERIC_ABSTRACT"); - } - } - - private boolean genericCoordinateType(TypeId id,Set seen) { - if(!seen.add(id))return false; - if(types.get(id) instanceof DomainReferenceType reference) { - var domain=symbols.get(reference.targetDomain()); - return domain instanceof DomainSymbol d && (d.generic() || genericCoordinateType(d.typeId(),seen)); - } - return false; - } - - private boolean mandatoryDomain(SymbolId domain) { - Set seen = new HashSet<>(); - for (SymbolId current = domain; current != null && seen.add(current); current = baseOf.get(current)) { - checkWork(); - if (index.byId(current).filter(d -> d.modifiers().contains(guru.interlis.model.Modifier.MANDATORY)).isPresent()) return true; - if (symbols.get(current) instanceof DomainSymbol d && d.mandatory()) return true; - } - return false; - } - - private boolean mandatoryType(TypeId type) { - Set seen = new HashSet<>(); - while (type != null && seen.add(type) && types.get(type) instanceof DomainReferenceType reference) { - if (mandatoryDomain(reference.targetDomain())) return true; - type = declaredTypes.get(reference.targetDomain()); - } - return false; - } - - /** Complete only owned inline types; referenced alias descriptors are never mutated. */ - private void inheritTypeProperties(TypeId id, TypeSyntax syntax, TypeDescriptor base,DeclInfo declaration) { - if (syntax instanceof NumericTypeSyntax own && types.get(id) instanceof NumericType numeric && base instanceof NumericType parent) { - var range=inheritRange(numeric.range(),parent.range(),own);var errors=new ArrayList(); - validateNumericUnit(range,declaration,syntax.range(),errors); - types.put(id,errors.isEmpty()?new NumericType(id,numeric.origin(),range):new ErrorType(id,numeric.origin(),errors)); - } - else if(syntax instanceof CoordinateTypeSyntax own && types.get(id) instanceof CoordinateType coordinate && base instanceof CoordinateType parent) { - List axes=new ArrayList<>(coordinate.axes()); - for(int i=0;i(); - for(int i=0;i> inferredDerivations = new LinkedHashMap<>(); - - boolean assignable(TypeId actual, TypeId expected) { - if(mandatoryType(expected) && guru.interlis.model.internal.TypeCompatibility.isUndefined(effectiveDescriptor(actual)))return false; - return guru.interlis.model.internal.TypeCompatibility.assign(effectiveDescriptor(actual), effectiveDescriptor(expected), this::effectiveDescriptor, baseOf::get, domain -> effectiveDescriptor(declaredTypes.get(domain))).decision() == guru.interlis.model.RelationDecision.ALLOWED; - } - - private List bindAttributeDerivations(DeclInfo decl, AttributeDeclarationSyntax syntax, TypeId type) { - if (inferredDerivations.containsKey(decl.id())) return inferredDerivations.get(decl.id()); - List result = new ArrayList<>(); - for (var expression : syntax.derivations()) { - var bound = contextualValue(buildExpression(expression, decl.owner().orElseThrow()),type,decl.id()); - result.add(bound.id()); - if(!bound.error() && (syntax.mandatory() || mandatoryType(type)) - && guru.interlis.model.internal.TypeCompatibility.isUndefined(effectiveDescriptor(bound.typeId()))) - emitRule(decl,"MANDATORY attribute excludes an explicitly undefined derived value","MANDATORY_VALUE",expression.range()); - if (!bound.error() && !assignable(bound.typeId(), type)) - emitTypeMismatch(decl.sourceId(), expression.range(), typeName(bound.typeId()), typeName(type)); - } - return List.copyOf(result); - } - - /** Resolve once so every expression and the published attribute share the same type. */ - TypeId attributeType(DeclInfo decl) { - TypeId existing = declaredTypes.get(decl.id()); - if (existing != null) return existing; - checkWork(); - AttributeDeclarationSyntax syntax = (AttributeDeclarationSyntax) decl.syntax(); - TypeId type; - if (!resolvingAttributeTypes.add(decl.id())) { - type = nextTypeId(); - types.put(type, new ErrorType(type, decl.origin(), - List.of(emitRule(decl, "cyclic inherited attribute type", "INHERITANCE_CYCLE")))); - return type; - } - try { - decl.owner().flatMap(this::objectInheritanceBase).flatMap(base->findMember(base,decl.name())) - .filter(base->symbols.get(base) instanceof AttributeSymbol || index.byId(base).filter(d->d.kind()==SymbolKind.ATTRIBUTE).isPresent()) - .ifPresent(base->baseOf.put(decl.id(),base)); - if (syntax.type() instanceof GenericTypeSyntax inferred && "INFERRED".equals(inferred.name())) { - var result = buildExpression(syntax.derivations().getFirst(), decl.owner().orElseThrow()); - inferredDerivations.put(decl.id(), List.of(result.id())); - type = result.typeId(); - } else if (syntax.type() instanceof GenericTypeSyntax generic && "INHERITED".equals(generic.name())) { - Optional base = decl.owner().flatMap(this::objectInheritanceBase) - .flatMap(owner -> findMember(owner, decl.name())); - DeclInfo inherited = base.flatMap(index::byId).orElse(null); - if (inherited != null && inherited.kind() == SymbolKind.ATTRIBUTE) { - type = attributeType(inherited); - } else if (base.map(symbols::get).orElse(null) instanceof AttributeSymbol attribute) { - // ALL OF members have a bound type but no separate syntax declaration. - type = attribute.declaredType(); - } else { - type = nextTypeId(); - types.put(type, new ErrorType(type, decl.origin(), List.of(emitRule(decl, - "MANDATORY without a type requires an inherited attribute", "ATTRIBUTE_INHERITED_TYPE")))); - } - } else { - type = buildType(syntax.type(), decl.id(), decl.sourceId()); - TypeDescriptor inheritedGeometry = null; - if (decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - var base = decl.owner().flatMap(this::objectInheritanceBase).flatMap(owner -> findMember(owner, decl.name())); - TypeId baseType = base.flatMap(index::byId).filter(d -> d.kind() == SymbolKind.ATTRIBUTE) - .map(this::attributeType).orElseGet(() -> base.map(symbols::get) - .filter(AttributeSymbol.class::isInstance).map(AttributeSymbol.class::cast) - .map(AttributeSymbol::declaredType).orElse(null)); - inheritTypeProperties(type, syntax.type(), effectiveDescriptor(baseType),decl); - inheritedGeometry = effectiveDescriptor(baseType); - } - if (syntax.type() instanceof LineTypeSyntax) lines.effective(type, inheritedGeometry, decl); - } - declaredTypes.put(decl.id(), type); - return type; - } finally { - resolvingAttributeTypes.remove(decl.id()); - } - } - - private RoleSymbol buildRole(DeclInfo decl) { - RoleDeclarationSyntax syntax = (RoleDeclarationSyntax) decl.syntax(); - List targets = new ArrayList<>(); - for (var target : syntax.targets()) { - checkWork(); - SymbolId base = bindRoleTarget(decl, target.base()); - List restrictions = target.restrictions().stream() - .map(name -> bindRoleTarget(decl, name)).toList(); - for (SymbolId restriction : restrictions) { - if (!targetSpecializes(restriction, base)) { - emitRule(decl, "restriction must specialize its role target", "ROLE_TARGET_RESTRICTION"); - } - } - for (var previous : targets) { - if (targetSpecializes(base, previous.base()) || targetSpecializes(previous.base(), base)) { - emitRule(decl, "role alternatives must not specialize each other", "ROLE_TARGET_OVERLAP"); - } - } - targets.add(new guru.interlis.model.RoleTarget(base, restrictions)); - } - Cardinality cardinality = syntax.cardinality(); - SymbolId associationBase = baseOf.get(decl.owner().orElseThrow()); - RoleSymbol inherited = associationBase == null ? null : associationRoles(associationBase).stream() - .filter(r -> r.name().value().equals(decl.name())).findFirst().orElse(null); - if (inherited == null) { - if (associationBase != null) emitRule(decl, "extended association must not add roles", "ASSOCIATION_EXTENDED_ROLE"); - if (decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED) - || decl.modifiers().contains(guru.interlis.model.Modifier.HIDING)) { - emitRule(decl, "role extension requires an inherited role", "ROLE_EXTENSION_BASE"); - } - } else { - if (!decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - emitRule(decl, "role override requires EXTENDED", "ROLE_EXTENSION"); - } - if (inherited.header().modifiers().contains(guru.interlis.model.Modifier.FINAL)) { - emitRule(decl, "cannot override FINAL role", "ROLE_FINAL"); - } - if (!syntax.explicitCardinality()) cardinality = inherited.cardinality(); - if (cardinality.lower().compareTo(inherited.cardinality().lower()) < 0 - || widerUpper(cardinality.upper(), inherited.cardinality().upper()) - || syntax.aggregation().ordinal() < inherited.aggregation().ordinal()) { - emitRule(decl, "extended role widens cardinality or weakens aggregation", "ROLE_EXTENSION"); - } - if (inherited.external() != decl.modifiers().contains(guru.interlis.model.Modifier.EXTERNAL)) { - emitRule(decl, "EXTERNAL must remain unchanged in an extended role", "ROLE_EXTERNAL_EXTENSION"); - } - if (inherited.ordered() && !decl.modifiers().contains(guru.interlis.model.Modifier.ORDERED)) { - emitRule(decl, "an ordered role cannot become unordered in an extension", "ROLE_ORDER_EXTENSION"); - } - if (targets.stream().flatMap(t -> t.allowedRoots().stream()).anyMatch(t -> !roleAccepts(inherited, t))) { - emitRule(decl, "extended role widens its target set", "ROLE_TARGET_EXTENSION"); - } - } - return new RoleSymbol(header(decl), targets, cardinality, syntax.aggregation(), - decl.modifiers().contains(guru.interlis.model.Modifier.ORDERED), - decl.modifiers().contains(guru.interlis.model.Modifier.EXTERNAL), - decl.modifiers().contains(guru.interlis.model.Modifier.HIDING), bindRoleDerivation(decl, syntax, targets)); - } - - private Optional bindRoleDerivation(DeclInfo decl, RoleDeclarationSyntax syntax, List targets) { - return syntax.derivation().map(expression -> { - var bound = buildExpression(expression, decl.owner().orElseThrow()); - var actual = effectiveDescriptor(bound.typeId()); - if (!bound.error()) { - var roots = targets.stream().flatMap(t -> t.allowedRoots().stream()).toList(); - var expected = new guru.interlis.model.ObjectArgumentType(nextTypeId(), decl.origin(), Optional.empty(), roots, false); - types.put(expected.id(), expected); - if (!assignable(bound.typeId(), expected.id())) emitTypeMismatch(decl.sourceId(), expression.range(), typeName(bound.typeId()), "single role target"); - } - return bound.id(); - }); - } - - private List associationRoles(SymbolId association) { - Map roles = new LinkedHashMap<>(); - List chain = baseChain(association); - for (int i = chain.size() - 1; i >= 0; i--) { - for (DeclInfo child : index.children(chain.get(i))) { - checkWork(); - if (child.kind() == SymbolKind.ROLE) { - roles.put(child.name(), (RoleSymbol) buildSymbol(child)); - } - } - } - return List.copyOf(roles.values()); - } - - boolean targetSpecializes(SymbolId candidate, SymbolId base) { - return objectBase(base).isEmpty() - || guru.interlis.model.internal.TypeCompatibility.specializes(candidate, base, baseOf::get); - } - - private boolean roleAccepts(RoleSymbol role, SymbolId candidate) { - return role.targetRoots().stream().anyMatch(base -> targetSpecializes(candidate, base)); - } - - private SymbolId bindObjectTarget(DeclInfo decl, guru.interlis.syntax.NameSyntax name, ReferenceUsage usage) { - Optional target = name.text().equals("ANYCLASS") ? Optional.of(anyClass(decl)) - : resolver.resolve(decl.id(), name, Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION)).target(); - if (target.isEmpty() && name.segments().size() == 1) { - target = baseTopicMember(decl.id(), name.text(), Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION)); - } - if (target.isEmpty()) unresolved(name); - recordReference(decl, name, target.orElse(null), usage); - return target.orElseGet(() -> syntheticClass(decl, name)); - } - - private SymbolId bindRoleTarget(DeclInfo decl, guru.interlis.syntax.NameSyntax name) { - var target=bindObjectTarget(decl, name, ReferenceUsage.ROLE_TARGET); - return target; - } - - List topicDependencies(SymbolId topic) { - Set result = new LinkedHashSet<>(); - for (SymbolId id : baseChain(topic)) { - checkWork(); - if (symbols.get(id) instanceof TopicSymbol owner) result.addAll(owner.dependencies()); - } - return List.copyOf(result); - } - - private void validateTopicDependencies() { - for (Symbol symbol : symbols.values()) { - if (!(symbol instanceof TopicSymbol topic)) continue; - checkWork(); - Set seen = new HashSet<>(); - var pending = new java.util.ArrayDeque<>(topicDependencies(topic.id())); - while (!pending.isEmpty()) { - checkWork(); - SymbolId next = pending.removeFirst(); - if (next.equals(topic.id())) { - emitRule(index.byId(topic.id()).orElseThrow(), "cyclic topic dependency", "TOPIC_DEPENDENCY_CYCLE"); - break; - } - if (seen.add(next)) pending.addAll(topicDependencies(next)); - } - } - } - - private void validateReferenceScope(DeclInfo site, SymbolId sourceTopic, SymbolId target, - boolean external, String externalRule) { - SymbolId targetTopic = scopeRoot(target); - if (!(symbols.get(sourceTopic) instanceof TopicSymbol) - || !(symbols.get(targetTopic) instanceof TopicSymbol) - || isTopicSubtype(sourceTopic, targetTopic)) return; - if (!external) emitRule(site, "cross-topic reference requires EXTERNAL", externalRule); - if (!topicDependencies(sourceTopic).contains(targetTopic)) - emitRule(site, "reference target topic requires DEPENDS ON", "REFERENCE_DEPENDS_ON"); - } - - private boolean shareBaseTopic(SymbolId first, SymbolId second) { - Set firstBases = new LinkedHashSet<>(); - SymbolId current = first; - int guard = 0; - while (current != null && guard++ < 32) { - checkWork(); - firstBases.add(current); - current = symbols.get(current) instanceof TopicSymbol topic - ? topic.baseTopic().orElse(null) : null; - } - current = second; - guard = 0; - while (current != null && guard++ < 32) { - checkWork(); - if (firstBases.contains(current)) { - return true; - } - current = symbols.get(current) instanceof TopicSymbol topic - ? topic.baseTopic().orElse(null) : null; - } - return false; - } - - boolean isTopicSubtype(SymbolId candidate, SymbolId base) { - SymbolId current = candidate; - Set visited = new LinkedHashSet<>(); - while (current != null && visited.add(current)) { - checkWork(); - if (current.equals(base)) { - return true; - } - Symbol symbol = symbols.get(current); - if (symbol instanceof TopicSymbol topic) { - current = topic.baseTopic().orElse(null); - } else { - return false; - } - } - return false; - } - - private SymbolId anyClass(DeclInfo owner) { - SymbolId model = index.modelOfSymbol(owner.id()).orElseThrow(); - SymbolId id = new SymbolId(model.value() + ":predefined:ANYCLASS"); - symbols.computeIfAbsent(id, ignored -> new ClassSymbol(new SymbolHeader(id, SymbolKind.CLASS, - DeclaredName.declared("ANYCLASS"), Optional.of(model), 0, - new Origin.PredefinedOrigin(owner.language(), "INTERLIS.ANYCLASS"), Optional.empty(), - owner.language(), Documentation.empty(), MetaAttributeSet.empty(), Set.of(), List.of()), - Optional.empty(), List.of(), List.of())); - return id; - } - - private Optional objectBase(SymbolId target) { - Symbol symbol = symbols.get(target); - return symbol != null && symbol.origin() instanceof Origin.PredefinedOrigin predefined - && predefined.key().equals("INTERLIS.ANYCLASS") ? Optional.empty() : Optional.of(target); - } - - private SymbolId syntheticClass(DeclInfo owner, guru.interlis.syntax.NameSyntax name) { - SymbolId id = new SymbolId("sym-unresolved-" + (++typeCounter)); - SymbolId model = index.modelOfSymbol(owner.id()).orElse(owner.id()); - SymbolHeader header = new SymbolHeader(id, SymbolKind.CLASS, - DeclaredName.synthetic(""), - Optional.of(model), 0, - new Origin.SyntheticOrigin(guru.interlis.model.SyntheticReason.MISSING_REFERENCE, - Optional.of(name.range())), - Optional.of(name.range()), owner.language(), Documentation.empty(), - MetaAttributeSet.empty(), Set.of(), List.of()); - ClassSymbol synthetic = new ClassSymbol(header, Optional.empty(), - List.of(), List.of()); - symbols.put(id, synthetic); - return id; - } - - private ConstraintSymbol buildConstraint(DeclInfo decl) { - ConstraintDeclarationSyntax syntax; - Optional externalTarget = Optional.empty(); - if (decl.syntax() instanceof ExternalConstraintSyntax external) { - syntax = external.constraint(); - externalTarget = tryResolve(decl, external.target(), - Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION)); - if (externalTarget.isEmpty() && external.target().segments().size() == 1) { - externalTarget = baseTopicMember(decl.id(), external.target().text(), - Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION)); - } - if (externalTarget.isEmpty()) { - unresolved(external.target()); - } else { - SymbolId target = externalTarget.orElseThrow(); - recordReference(decl, external.target(), target, ReferenceUsage.EXPRESSION); - boolean inherited = baseTopicMember(decl.id(), - index.byId(target).orElseThrow().name(), - Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION)).filter(target::equals).isPresent(); - if (!Objects.equals(scopeRoot(decl.id()), scopeRoot(target)) && !inherited) { - emitRule(decl, "external constraints must be declared in the target's topic", - "CONSTRAINT_TARGET_TOPIC"); - } - } - } else { - syntax = (ConstraintDeclarationSyntax) decl.syntax(); - } - externalTarget.ifPresent(target -> externalConstraintTargets.put(decl.id(), target)); - SymbolId context = contextualObject(expressionContext(decl.id()), scopeRoot(decl.id())); - if (syntax.existence().isPresent()) { - var existence = syntax.existence().orElseThrow(); - ExpressionResult source = constraintPath(existence.source(), decl, context, false); - requireConstraintValue(decl, source, false); - List targets = new ArrayList<>(); - for (var target : existence.targets()) { - checkWork(); - var resolved = resolver.resolve(decl.id(), target.viewable(), - Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION, SymbolKind.VIEW), false).target(); - if (resolved.isEmpty() && target.viewable().segments().size() == 1) - resolved = baseTopicMember(decl.id(), target.viewable().text(), - Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION, SymbolKind.VIEW)); - recordReference(decl, target.viewable(), resolved.orElse(null), ReferenceUsage.EXPRESSION); - if (resolved.isEmpty()) { unresolved(target.viewable()); continue; } - SymbolId targetScope = resolved.orElseThrow(); - SymbolId ownerTopic = scopeRoot(decl.id()); - SymbolId targetTopic = scopeRoot(targetScope); - boolean depends = ownerTopic == null || targetTopic == null - || index.byId(ownerTopic).map(d -> d.kind() != SymbolKind.TOPIC).orElse(true) - || index.byId(targetTopic).map(d -> d.kind() != SymbolKind.TOPIC).orElse(true) - || topicAvailable(ownerTopic, targetTopic) - || baseChain(ownerTopic).stream().map(index::byId).flatMap(Optional::stream) - .filter(d -> d.syntax() instanceof TopicSyntax) - .anyMatch(d -> ((TopicSyntax) d.syntax()).dependencies().stream() - .anyMatch(n -> resolver.resolve(d.id(), n, Set.of(SymbolKind.TOPIC)).target() - .filter(targetTopic::equals).isPresent())); - if (!depends) emitRule(decl, "EXISTENCE target topic requires DEPENDS ON", "EXISTENCE_DEPENDS"); - ExpressionResult path = constraintPath(target.path(), decl, targetScope, false); - requireConstraintValue(decl, path, false); - if (!source.error() && !path.error() && !existenceCompatible(source.typeId(), path.typeId())) { - TypeDescriptor from = effectiveDescriptor(source.typeId()), to = effectiveDescriptor(path.typeId()); - boolean familyPending = from.kind() == to.kind() && switch (from.kind()) { - case GENERIC, DEFERRED, PENDING -> true; - default -> false; - }; - if (familyPending) emitUnsupported(decl.sourceId(), target.path().range(), - "EXISTENCE_TYPE_COMPATIBILITY", "EXISTENCE compatibility for " + from.kind()); - else emitTypeMismatch(decl.sourceId(), target.path().range(), typeName(source.typeId()), typeName(path.typeId())); - } - targets.add(new guru.interlis.model.ExistenceConstraint.ExistenceTarget(targetScope, path.id())); - } - return new ConstraintSymbol(header(decl), new guru.interlis.model.ExistenceConstraint( - syntax.name(), source.id(), targets), externalTarget); - } - if (syntax.constraintKind() == guru.interlis.syntax.ConstraintKind.SET - && decl.owner().flatMap(index::byId).filter(d -> d.kind() == SymbolKind.STRUCTURE).isPresent()) - emitRule(decl, "SET CONSTRAINT is not allowed in a structure", "SET_IN_STRUCTURE"); - if (syntax.constraintKind() == guru.interlis.syntax.ConstraintKind.UNIQUENESS) { - Optional where = syntax.expression().map(e -> { - ExpressionResult condition = buildExpression(e, decl.id()); - if (!condition.error() && !isBoolean(condition.typeId())) - emitTypeMismatch(decl.sourceId(), e.range(), typeName(condition.typeId()), "BOOLEAN"); - return condition.id(); - }); - List keys = new ArrayList<>(); - List localKeys = new ArrayList<>(); - if (syntax.localPrefix().isPresent()) { - if (syntax.basket()) emitRule(decl, "UNIQUE cannot combine BASKET and LOCAL", "UNIQUE_KEY"); - ExpressionResult prefix = constraintPath(syntax.localPrefix().orElseThrow(), decl, context, true); - SymbolId structure = structures.target(effectiveDescriptor(prefix.typeId())); - if (!prefix.error() && structure == null) - emitRule(decl, "LOCAL prefix must select a structure attribute", "UNIQUE_LOCAL_PREFIX"); - for (var key : syntax.uniqueKeys()) { - ExpressionResult field = constraintPath(key, decl, structure, false); - requireConstraintValue(decl, field, true); - keys.add(field.id()); - } - localKeys.add(new UniquenessConstraint.LocalUniqueness(prefix.id(), keys)); - keys = List.of(); - } else { - if (context != null && index.byId(context).filter(d -> d.kind() == SymbolKind.STRUCTURE).isPresent()) - emitRule(decl, "global UNIQUE requires identifiable objects", "UNIQUE_STRUCTURE"); - for (var key : syntax.uniqueKeys()) { - ExpressionResult path = constraintPath(key, decl, context, false); - requireConstraintValue(decl, path, true); - keys.add(path.id()); - } - } - return new ConstraintSymbol(header(decl), new UniquenessConstraint(syntax.name(), - syntax.basket(), where, keys, localKeys), externalTarget); - } - Optional setWhere=syntax.where().map(e->{ - var condition=buildExpression(e,decl.id()); - if(!condition.error() && !isBoolean(condition.typeId()))emitTypeMismatch(decl.sourceId(),e.range(),typeName(condition.typeId()),"BOOLEAN"); - return condition.id(); - }); - if(syntax.constraintKind()==guru.interlis.syntax.ConstraintKind.PLAUSIBILITY && syntax.plausibility().isEmpty()) { - var error=missingConstraintExpression(decl,syntax.range()); - return new ConstraintSymbol(header(decl),new MandatoryConstraint(syntax.name(),error.id()),externalTarget); - } - SymbolId expressionOwner = decl.id(); - ExpressionResult expression = syntax.expression() - .map(e -> buildExpression(e, expressionOwner)) - .orElseGet(() -> missingConstraintExpression(decl,syntax.range())); - if (!expression.error() && !isBoolean(expression.typeId())) { - emitTypeMismatch(decl.sourceId(), syntax.range(), typeName(expression.typeId()), - "BOOLEAN"); - } - if (!expression.error() && isBoolean(expression.typeId()) && syntax.constraintKind()==guru.interlis.syntax.ConstraintKind.SET - && decl.language()==guru.interlis.model.LanguageVersion.ILI24 && !hasObjectSetFunction(expression.id())) { - var cause=emitTypeMismatch(decl.sourceId(),expressions.get(expression.id()).range(), - "SET condition without OBJECTS function","SET condition with OBJECTS function"); - expression=failedFunctionArgument(expressions.get(expression.id()).range(),List.of(cause)); - } - ConstraintDefinition definition = switch (syntax.constraintKind()) { - case MANDATORY -> new MandatoryConstraint(syntax.name(), expression.id()); - case PLAUSIBILITY -> new PlausibilityConstraint(syntax.name(), - syntax.plausibility().orElseThrow().percentage().exactValue(), syntax.plausibility().orElseThrow().comparison(), - expression.id()); - case DOMAIN -> new DomainConstraint(expression.id()); - case SET -> new guru.interlis.model.SetConstraint(syntax.name(), - syntax.basket()?guru.interlis.model.SetConstraint.SetConstraintScope.BASKET: guru.interlis.model.SetConstraint.SetConstraintScope.GLOBAL, setWhere, - expression.id()); - default -> new MandatoryConstraint(syntax.name(), expression.id()); - }; - return new ConstraintSymbol(header(decl), definition, externalTarget); - } - - /** 2.4 requires an actual call with an OBJECTS formal somewhere in the set - * condition; an ALL value or such a call only in WHERE does not suffice. */ - private boolean hasObjectSetFunction(ExpressionId root) { - var pending=new java.util.ArrayDeque();var seen=new java.util.HashSet();pending.add(root); - while(!pending.isEmpty()) { - checkWork();var id=pending.removeFirst();if(!seen.add(id))continue;var expression=expressions.get(id); - if(expression instanceof guru.interlis.model.FunctionCallExpression call - && call.targetFunction().map(symbols::get).filter(FunctionSymbol.class::isInstance).map(FunctionSymbol.class::cast) - .stream().flatMap(f->f.parameters().stream()).map(p->effectiveDescriptor(p.typeId())) - .anyMatch(t->t instanceof guru.interlis.model.ObjectArgumentType objects&&objects.objects()))return true; - pending.addAll(expression.children()); - } - return false; - } - - private boolean widerUpper(guru.interlis.model.CardinalityUpper left, - guru.interlis.model.CardinalityUpper right) { - if (left instanceof guru.interlis.model.CardinalityUpper.Unbounded) { - return !(right instanceof guru.interlis.model.CardinalityUpper.Unbounded); - } - if (right instanceof guru.interlis.model.CardinalityUpper.Unbounded) { - return false; - } - if (left instanceof guru.interlis.model.CardinalityUpper.Finite leftFinite - && right instanceof guru.interlis.model.CardinalityUpper.Finite rightFinite) { - return leftFinite.value().compareTo(rightFinite.value()) > 0; - } - return false; - } - - private ExpressionResult constraintPath(ExpressionSyntax syntax, DeclInfo owner, - SymbolId start, boolean localPrefix) { - if (syntax instanceof PathExpressionSyntax path) - return pathExpression(new ExpressionId("expr-" + (++expressionCounter)), path, owner.id(), start, localPrefix); - return buildExpression(syntax, owner.id()); // Unsupported operators keep their explicit diagnostic. - } - - private void requireConstraintValue(DeclInfo owner, ExpressionResult result, boolean unique) { - if (result.error()) return; - TypedExpression expression = expressions.get(result.id()); - TypeDescriptor type = effectiveDescriptor(result.typeId()); - boolean invalid = unique && type instanceof CollectionType || type instanceof guru.interlis.model.ObjectArgumentType o && o.objects(); - if (unique) invalid |= type instanceof StructureValueType; - else invalid |= !(expression instanceof guru.interlis.model.PathExpression p - && (p.path().steps().getLast() instanceof guru.interlis.model.PathStep.AttributeStep - || p.path().steps().getLast() instanceof guru.interlis.model.PathStep.IndexStep)); - if (invalid) emitRule(owner, unique ? "UNIQUE key must be a singular non-structure value" - : "EXISTENCE path must select one attribute value", unique ? "UNIQUE_KEY" : "EXISTENCE_PATH"); - } - - // EXISTENCE compares values, it does not assign the complete source domain. - // Numeric/text bounds are data restrictions, not an assignment precondition. - private boolean existenceCompatible(TypeId source, TypeId target) { - TypeDescriptor actual = effectiveDescriptor(source); - TypeDescriptor expected = effectiveDescriptor(target); - if (actual instanceof CollectionType a && expected instanceof CollectionType b) - return a.collectionKind() == b.collectionKind() && existenceCompatible(a.elementType(), b.elementType()); - if (actual instanceof CollectionType a && !isMany(a.cardinality())) actual = effectiveDescriptor(a.elementType()); - if (expected instanceof CollectionType b && !isMany(b.cardinality())) expected = effectiveDescriptor(b.elementType()); - if (actual == null || expected == null) return false; - if (actual instanceof guru.interlis.model.OidType a && expected instanceof guru.interlis.model.OidType b) - return a.oidKind() == guru.interlis.model.OidKind.ANY || b.oidKind() == guru.interlis.model.OidKind.ANY - || a.oidKind() == b.oidKind() && (a.valueType().isEmpty() && b.valueType().isEmpty() - || a.valueType().isPresent() && b.valueType().isPresent() - && existenceCompatible(a.valueType().orElseThrow(), b.valueType().orElseThrow())); - if (actual.id().equals(expected.id())) return true; - if (actual instanceof GeometryType geometry && expected instanceof CoordinateType) { - actual = geometry.vertexDomain().map(declaredTypes::get).map(this::effectiveDescriptor).orElse(null); - } - if (actual instanceof NumericType a && expected instanceof NumericType b) - return numericValueCompatible(a.range(), b.range()); - if (actual instanceof TextType && expected instanceof TextType) return true; - if (actual instanceof CoordinateType a && expected instanceof CoordinateType b) { - if (a.dimensions() != b.dimensions() || a.multiCoordinate() != b.multiCoordinate() - || !a.referenceSystem().equals(b.referenceSystem())) return false; - for (int i = 0; i < a.dimensions(); i++) - if (!numericValueCompatible(a.axes().get(i), b.axes().get(i))) return false; - return true; - } - if (actual instanceof guru.interlis.model.AlignmentType a && expected instanceof guru.interlis.model.AlignmentType b) - return a.alignment() == b.alignment(); - if (actual instanceof guru.interlis.model.BlackboxType a && expected instanceof guru.interlis.model.BlackboxType b) - return a.blackboxKind() == b.blackboxKind(); - if (actual instanceof FormattedType a && expected instanceof FormattedType b) { - if (a.predefined().isPresent() || b.predefined().isPresent()) return a.predefined().equals(b.predefined()); - return a.structure().equals(b.structure()) && a.parts().stream().map(p -> List.of(p.attribute(), p.width(), p.format(), p.literal())).toList() - .equals(b.parts().stream().map(p -> List.of(p.attribute(), p.width(), p.format(), p.literal())).toList()); - } - if (actual instanceof EnumerationType a && expected instanceof EnumerationType b) - return guru.interlis.model.internal.TypeCompatibility.relatedEnumerations(a, b, this::effectiveDescriptor) || a.ordered() == b.ordered() && a.circular() == b.circular() - && a.values().stream().map(EnumerationValue::path).toList() - .equals(b.values().stream().map(EnumerationValue::path).toList()); - return guru.interlis.model.internal.TypeCompatibility.assign(actual, expected, - this::effectiveDescriptor, baseOf::get, domain -> effectiveDescriptor(declaredTypes.get(domain))).decision() == guru.interlis.model.RelationDecision.ALLOWED; - } - - private boolean numericValueCompatible(NumericRange a, NumericRange b) { - return a.unit().equals(b.unit()) && a.referenceSystem().equals(b.referenceSystem()) - && a.direction() == b.direction() && a.circular() == b.circular(); - } - - private EnumElementSymbol buildEnumElement(DeclInfo decl) { - String path = decl.name(); - List segments = List.of(path.split("\\.")); - TypeDescriptor type = decl.owner().map(declaredTypes::get).map(this::effectiveDescriptor).orElse(null); - int ordinal = type instanceof EnumerationType enumeration ? enumeration.values().stream() - .filter(value -> value.path().equals(path)).mapToInt(EnumerationValue::order).findFirst().orElse(-1) : -1; - if (ordinal < 0) { - // Erroneous partial types still retain local declaration order, never - // the unrelated global declaration counter. - ordinal = index.children(decl.owner().orElseThrow()).stream().filter(d -> d.kind() == SymbolKind.ENUM_ELEMENT) - .map(DeclInfo::id).toList().indexOf(decl.id()); - } - return new EnumElementSymbol(header(decl), segments, ordinal); - } - - private UnsupportedSymbol buildUnsupported(DeclInfo decl) { - String feature = decl.featureId().orElse("UNKNOWN_SYNTAX"); - unsupportedBySource.computeIfAbsent(decl.sourceId(), ignored -> new LinkedHashSet<>()) - .add(feature); - emitUnsupported(decl.sourceId(), decl.origin() instanceof Origin.DeclaredOrigin d - ? d.range() : null, feature, decl.name()); - return new UnsupportedSymbol(header(decl), feature, decl.name()); - } - - private final ViewBuilder views = new ViewBuilder(this); - - private ViewSymbol buildView(DeclInfo decl) { return views.build(decl); } - - - - private void bindOidRules() { - Map> bound = new LinkedHashMap<>(); - for (Symbol symbol : List.copyOf(symbols.values())) { - if (symbol instanceof ClassSymbol || symbol instanceof TopicSymbol || symbol instanceof AssociationSymbol) - bindOidRules(symbol.id(), bound, new HashSet<>()); - } - } - - private List bindOidRules(SymbolId id, - Map> bound, Set active) { - checkWork(); - if (bound.containsKey(id)) return bound.get(id); - if (!active.add(id)) return List.of(); // Existing inheritance checks diagnose the cycle. - Symbol symbol = symbols.get(id); - DeclInfo decl = index.byId(id).orElse(null); - if (decl == null || !(symbol instanceof ClassSymbol || symbol instanceof TopicSymbol || symbol instanceof AssociationSymbol)) - return List.of(); - List declared = switch (decl.syntax()) { - case ClassDeclarationSyntax c -> c.oidRules(); - case TopicSyntax t -> t.oidRules(); - case AssociationDeclarationSyntax a -> a.oidRules(); - default -> List.of(); - }; - List inherited = baseOf.containsKey(id) - ? bindOidRules(baseOf.get(id), bound, active) : List.of(); - List rules = new ArrayList<>(inherited); - if (!(symbol instanceof TopicSymbol) && rules.isEmpty()) { - SymbolId topic = scopeRoot(id); - if (topic != null && !topic.equals(id) && symbols.get(topic) instanceof TopicSymbol) - bindOidRules(topic, bound, active).stream().filter(r -> !r.basket()).forEach(rules::add); - } - for (var rule : declared) { - checkWork(); - if (symbol instanceof TopicSymbol && rule.domain().isEmpty()) { - emitRule(decl, "NO OID is only allowed on classes and associations", "OID_TOPIC"); - continue; - } - Optional domain = Optional.empty(); - if (rule.domain().isPresent()) { - var name = rule.domain().orElseThrow(); - domain=bindTarget(decl,name,Set.of(SymbolKind.DOMAIN),ReferenceUsage.TYPE_USE,new ArrayList<>()); - if (domain.isEmpty()) continue; - if (!(effectiveDescriptor(declaredTypes.get(domain.orElseThrow())) instanceof guru.interlis.model.OidType)) { - emitRule(decl, "OID AS must bind an OID domain", "OID_DOMAIN"); continue; - } - } - var own = new guru.interlis.model.OidRule(rule.basket(), domain, new Origin.DeclaredOrigin(rule.range())); - var previous = inherited.stream().filter(r -> r.basket() == own.basket()).findFirst(); - if (previous.isPresent() && !oidOverrideAllowed(previous.orElseThrow(), own, decl.language(), symbol instanceof TopicSymbol)) - emitRule(decl, "OID rule cannot change its inherited domain", "OID_OVERRIDE"); - rules.removeIf(r -> r.basket() == own.basket()); rules.add(own); - } - // Only 2.4 defines an omitted class rule as inherited NO OID. - if (decl.language() == guru.interlis.model.LanguageVersion.ILI24 - && symbols.get(baseOf.get(id)) instanceof ClassSymbol && inherited.isEmpty() - && rules.stream().anyMatch(r -> !r.basket() && oidState(r) == 2)) - emitRule(decl, "inherited NO OID must first be concretized as ANYOID", "OID_OVERRIDE"); - rules.sort(Comparator.comparing(r -> !r.basket())); - for (var rule : rules) { - if (oidState(rule) == 1 && !decl.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) - emitRule(decl, "ANYOID requires an abstract topic, class or association", "OID_ABSTRACT"); - } - if (symbol instanceof AssociationSymbol association) { - Cardinality cardinality = association.cardinality(); - boolean multiple = !(cardinality.upper() instanceof guru.interlis.model.CardinalityUpper.Finite finite) - || finite.value().compareTo(java.math.BigInteger.ONE) > 0; - boolean identified = rules.isEmpty() - ? baseChain(id).stream().map(index::byId).flatMap(Optional::stream) - .anyMatch(d -> d.modifiers().contains(guru.interlis.model.Modifier.OID)) - : rules.stream().anyMatch(r -> !r.basket() && oidState(r) != 0); - if (multiple && !identified) - emitRule(decl, "multiple links for the same participants require OID", "ASSOCIATION_CARDINALITY_OID"); - } - var h = symbol.header(); - var header = new SymbolHeader(h.id(), h.kind(), h.name(), h.owner(), h.declarationOrder(), h.origin(), - h.nameRange(), h.languageVersion(), h.documentation(), h.metaAttributes(), h.modifiers(), rules); - Symbol replacement = switch (symbol) { - case ClassSymbol c -> new ClassSymbol(header, c.baseType(), c.members(), c.constraints()); - case TopicSymbol t -> new TopicSymbol(header, t.baseTopic(), t.members(), t.dependencies(), t.deferredGenerics(), t.genericBindings()); - case AssociationSymbol a -> new AssociationSymbol(header, a.baseAssociation(), a.cardinality(), a.roles(), a.attributes(), a.constraints(), a.derivedFrom()); - default -> symbol; - }; - symbols.put(id, replacement); active.remove(id); - var result = List.copyOf(rules); bound.put(id, result); return result; - } - - private void validateReferenceTargets() { - for (var entry : effectiveMembers.entrySet()) { - checkWork(); - SymbolId sourceOwner = entry.getKey(); - while (sourceOwner != null && index.byId(sourceOwner).isEmpty()) { - checkWork(); - sourceOwner = symbols.get(sourceOwner).owner().orElse(null); - } - if (sourceOwner == null) continue; // Predefined declarations are checked at their source uses. - DeclInfo owner = index.byId(sourceOwner).orElseThrow(); - SymbolId topic = scopeRoot(owner.id()); - Set seen = new HashSet<>(); - var pending = new java.util.ArrayDeque(); - pending.add(entry.getKey()); - while (!pending.isEmpty()) { - checkWork(); - SymbolId container = pending.removeFirst(); - if (!seen.add(container)) continue; - for (EffectiveMember member : effectiveMembers.getOrDefault(container, List.of())) { - checkWork(); - Symbol declaration = symbols.get(member.declaredSymbol()); - List roots = List.of(); - boolean external = false; - if (declaration instanceof RoleSymbol role) { - roots = role.targetRoots(); external = role.external(); - } - else if (declaration instanceof AttributeSymbol attribute) { - TypeDescriptor type = effectiveDescriptor(attribute.declaredType()); - while (type instanceof CollectionType collection) { - checkWork(); type = effectiveDescriptor(collection.elementType()); - } - if (type instanceof StructureValueType structure) pending.addAll(structure.targetRoots()); - if (type instanceof ReferenceType reference) { - roots = reference.targetRoots(); external = reference.external(); - } - } - DeclInfo site = declaration != null && declaration.header().owner().filter(owner.id()::equals).isPresent() - ? index.byId(declaration.id()).orElse(owner) : owner; - for (SymbolId root : roots) { - checkWork(); - Symbol target = symbols.get(contextualObject(root, topic)); - if (target == null || index.byId(target.id()).isEmpty()) continue; // ANYCLASS/unresolved: no invented OID. - validateReferenceScope(site, topic, target.id(), external, - declaration instanceof RoleSymbol ? "ROLE_EXTERNAL" : "REFERENCE_EXTERNAL"); - boolean objectOid = target.header().oidRules().stream().anyMatch(r -> !r.basket() && oidState(r) != 0); - if (target instanceof ClassSymbol && owner.language() == guru.interlis.model.LanguageVersion.ILI24 && !objectOid) - emitRule(site, "reference target " + target.name().value() + " has NO OID in INTERLIS 2.4", "REFERENCE_TARGET_NO_OID"); - if (target instanceof AssociationSymbol) { - boolean identified = target.header().oidRules().isEmpty() - ? baseChain(target.id()).stream().map(index::byId).flatMap(Optional::stream) - .anyMatch(d -> d.modifiers().contains(guru.interlis.model.Modifier.OID)) : objectOid; - if (!identified) emitRule(site, "association target " + target.name().value() + " requires its own OID", "REFERENCE_ASSOCIATION_OID"); - } - } - } - } - } - } - - private int oidState(guru.interlis.model.OidRule rule) { - if (rule.domain().isEmpty()) return 0; - Symbol domain = symbols.get(rule.domain().orElseThrow()); - if (domain != null && domain.name().value().equals("INTERLIS.NOOID")) return 0; - return effectiveDescriptor(declaredTypes.get(rule.domain().orElseThrow())) instanceof guru.interlis.model.OidType oid - && oid.oidKind() == guru.interlis.model.OidKind.ANY ? 1 : 2; - } - - private boolean oidOverrideAllowed(guru.interlis.model.OidRule base, guru.interlis.model.OidRule derived, - guru.interlis.model.LanguageVersion language, boolean topic) { - if (base.domain().equals(derived.domain())) return true; - if (base.domain().isPresent() && derived.domain().isPresent()) { - Symbol a = symbols.get(base.domain().orElseThrow()), b = symbols.get(derived.domain().orElseThrow()); - if (a != null && b != null && a.header().origin() instanceof Origin.SyntheticOrigin - && b.header().origin() instanceof Origin.SyntheticOrigin && a.name().equals(b.name())) return true; - } - int from = oidState(base), to = oidState(derived); - if (topic) return false; // A topic assignment is fixed, including ANYOID. - return from == 0 && to == 0 || from == 1 && to == 2 - || language == guru.interlis.model.LanguageVersion.ILI24 && from == 0 && to == 1; - } - - SymbolHeader header(DeclInfo decl) { - var values=new LinkedHashMap(); - var entries=new ArrayList(); - for(var attribute:decl.metadata().attributes()) { - var value=new MetaAttributeSet.Value(attribute.text(),new Origin.DeclaredOrigin(attribute.range())); - entries.add(new MetaAttributeSet.Entry(attribute.key(),value)); - var previous=values.putIfAbsent(attribute.key(),value); - boolean repeatable=attribute.key().equals("fn.param") && decl.syntax() instanceof FunctionDeclarationSyntax; - if(previous!=null && !previous.text().equals(attribute.text()) && !repeatable)emitRule(decl,"conflicting source metaattribute: "+attribute.key(),"METAATTRIBUTE_CONFLICT",attribute.range()); - } - var configured=overlays.metadata(decl.id()); - if(!configured.isEmpty()) { - var combined=new ArrayList(configured);combined.addAll(entries);entries=combined;values.clear(); - for(var entry:entries)values.putIfAbsent(entry.key(),entry.value()); - } - var documentation=new Documentation(decl.metadata().documentation().stream().map(guru.interlis.syntax.SourceMetadata.Text::text).collect(java.util.stream.Collectors.joining("\n\n")), - decl.metadata().documentation().stream().map(guru.interlis.syntax.SourceMetadata.Text::range).toList()); - DeclaredName declaredName = isSyntheticName(decl.name()) - ? DeclaredName.synthetic(decl.name()) : DeclaredName.declared(decl.name()); - Set modifiers = new LinkedHashSet<>(decl.modifiers()); - if (decl.syntax() instanceof AttributeDeclarationSyntax attribute - && attribute.transientAttribute()) { - modifiers.add(guru.interlis.model.Modifier.TRANSIENT); - } - if (decl.kind() == SymbolKind.DOMAIN && mandatoryDomain(decl.id())) modifiers.add(guru.interlis.model.Modifier.MANDATORY); - return new SymbolHeader(decl.id(), decl.kind(), declaredName, decl.owner(), - decl.order(), decl.origin(), decl.nameRange(), decl.language(), - documentation, new MetaAttributeSet(values,entries), modifiers, List.of()); - } - - private static boolean isSyntheticName(String name) { - return name.startsWith("<") || name.startsWith("constraint@") - || name.startsWith("unsupported@") || name.startsWith("Association@"); - } - - // ---- types ----------------------------------------------------------------- - - /** Simple predefined domains allowed by DomainRef. No fallback for unknown names. */ - private Optional builtinSimpleDomain(DeclInfo context, String name) { - if (!Set.of("INTERLIS.URI", "INTERLIS.NAME", "INTERLIS.BOOLEAN", "INTERLIS.INTERLIS_1_DATE", "INTERLIS.HALIGNMENT", "INTERLIS.VALIGNMENT").contains(name)) - return Optional.empty(); - SymbolId symbol = new SymbolId("predefined:" + context.language() + ":domain:" + name); - if (symbols.containsKey(symbol)) return Optional.of(symbol); - Origin origin = new Origin.PredefinedOrigin(context.language(), name); - TypeId type = nextTypeId(); - TypeDescriptor descriptor=switch(name) { - case "INTERLIS.URI" -> new TextType(type,origin,TextKind.URI,java.util.OptionalInt.of(1023)); - case "INTERLIS.NAME" -> new TextType(type,origin,TextKind.NAME,java.util.OptionalInt.of(255)); - case "INTERLIS.BOOLEAN" -> new guru.interlis.model.BooleanType(type,origin); - case "INTERLIS.INTERLIS_1_DATE" -> new TextType(type,origin,TextKind.TEXT,java.util.OptionalInt.of(8)); - default -> new guru.interlis.model.AlignmentType(type,origin,name.equals("INTERLIS.HALIGNMENT") - ? guru.interlis.model.AlignmentKind.HORIZONTAL : guru.interlis.model.AlignmentKind.VERTICAL); - }; - types.put(type,descriptor); - declaredTypes.put(symbol, type); - symbols.put(symbol, new DomainSymbol(new SymbolHeader(symbol, SymbolKind.DOMAIN, - DeclaredName.synthetic(name), Optional.empty(), 0, origin, Optional.empty(), context.language(), - Documentation.empty(), MetaAttributeSet.empty(), Set.of(guru.interlis.model.Modifier.FINAL), List.of()), - type, Optional.empty(), false)); - return Optional.of(symbol); - } - - private Optional builtinOidDomain(SymbolId context, String name) { - if (!Set.of("INTERLIS.NOOID", "INTERLIS.ANYOID", "INTERLIS.I32OID", "INTERLIS.STANDARDOID", "INTERLIS.UUIDOID").contains(name)) - return Optional.empty(); - var language = index.byId(context).orElseThrow().language(); - if (name.equals("INTERLIS.NOOID") && language == guru.interlis.model.LanguageVersion.ILI23) - return Optional.empty(); - SymbolId symbol = new SymbolId("predefined:" + language + ":domain:" + name); - if (symbols.containsKey(symbol)) return Optional.of(symbol); - Optional base = language == guru.interlis.model.LanguageVersion.ILI23 || name.equals("INTERLIS.NOOID") - ? Optional.empty() - : builtinOidDomain(context, name.equals("INTERLIS.ANYOID") ? "INTERLIS.NOOID" : "INTERLIS.ANYOID"); - Origin origin = new Origin.PredefinedOrigin(language, name); - var kind = name.equals("INTERLIS.I32OID") ? guru.interlis.model.OidKind.NUMERIC - : name.equals("INTERLIS.UUIDOID") ? guru.interlis.model.OidKind.UUID - : name.equals("INTERLIS.STANDARDOID") ? guru.interlis.model.OidKind.TEXT : guru.interlis.model.OidKind.ANY; - Optional carrier = Optional.empty(); - if (kind != guru.interlis.model.OidKind.ANY) { - TypeId value = nextTypeId(); - types.put(value, kind == guru.interlis.model.OidKind.NUMERIC - ? new NumericType(value, origin, NumericRange.of(guru.interlis.model.DecimalLiteral.parse("0"), guru.interlis.model.DecimalLiteral.parse("2147483647"))) - : new TextType(value, origin, TextKind.TEXT, java.util.OptionalInt.of(kind == guru.interlis.model.OidKind.UUID ? 36 : 16))); - carrier = Optional.of(value); - } - TypeId type = nextTypeId(); types.put(type, new guru.interlis.model.OidType(type, origin, kind, carrier)); - declaredTypes.put(symbol, type); base.ifPresent(b -> baseOf.put(symbol, b)); - symbols.put(symbol, new DomainSymbol(new SymbolHeader(symbol, SymbolKind.DOMAIN, DeclaredName.synthetic(name), - Optional.empty(), 0, origin, Optional.empty(), language, - Documentation.empty(), MetaAttributeSet.empty(), language == guru.interlis.model.LanguageVersion.ILI24 && name.equals("INTERLIS.ANYOID") - ? Set.of(guru.interlis.model.Modifier.ABSTRACT) : Set.of(), List.of()), type, base, false)); - return Optional.of(symbol); - } - - private Optional bindFormatName(SymbolId context,guru.interlis.syntax.NameSyntax name,Set kinds) { - var target=bindTarget(index.byId(context).orElseThrow(),name,kinds,ReferenceUsage.TYPE_USE,new ArrayList<>()); - target.flatMap(index::byId).filter(d->d.kind()==SymbolKind.DOMAIN).ifPresent(this::domainType); - return target; - } - - private TypeDescriptor buildFormattedType(TypeId id, FormattedTypeSyntax syntax, SymbolId context) { - DeclInfo owner = index.byId(context).orElseThrow(); - List errors=new ArrayList<>(); - Optional domain = syntax.basedOnStructure() || syntax.base().isEmpty() - ? Optional.ofNullable(baseOf.get(context)) : Optional.empty(); - Optional structure = Optional.empty(); - Optional predefined = Optional.empty(); - if (syntax.base().isPresent()) { - var name = syntax.base().orElseThrow(); - if (syntax.basedOnStructure()) structure = bindFormatName(context, name, Set.of(SymbolKind.STRUCTURE)); - else domain = bindFormatName(context, name, Set.of(SymbolKind.DOMAIN)); - } - TypeDescriptor inherited = domain.map(declaredTypes::get).map(this::effectiveDescriptor).orElse(null); - FormattedType base = inherited instanceof FormattedType f ? f : null; - List parts = new ArrayList<>(); - if (!syntax.basedOnStructure()) { - if (base == null) errors.add(emitRule(owner, "FORMAT requires a bound formatted base domain", "FORMAT_BASE")); - else { structure = base.structure(); predefined = base.predefined(); parts.addAll(base.parts()); } - } else if (syntax.inheritance()) { - if (base == null || structure.isEmpty() || base.structure().isEmpty() - || !targetSpecializes(structure.orElseThrow(), base.structure().orElseThrow())) - errors.add(emitRule(owner, "FORMAT INHERITANCE requires the base structure or a specialization", "FORMAT_INHERITANCE")); - else for (var part : base.parts()) { - // INHERITANCE keeps the layout and provenance, but uses the effective - // attribute of the specialized structure (including its precision). - Optional attribute = part.attribute().isEmpty() ? Optional.empty() - : findMember(structure.orElseThrow(), symbols.get(part.attribute().orElseThrow()).name().value()); - parts.add(new FormattedType.Part(attribute, part.width(), part.format(), part.literal(), part.origin())); - } - } else if (base != null) errors.add(emitRule(owner, "extended FORMAT must retain its inherited prefix", "FORMAT_INHERITANCE")); - for (var part : syntax.parts()) { - checkWork(); - Optional attribute = Optional.empty(), nested = Optional.empty(); - if (part.attribute().isEmpty()) { - if (!part.literal().isEmpty() && Character.isDigit(part.literal().charAt(0))) - errors.add(emitRule(owner, "FORMAT separator must not start with a digit", "FORMAT_SEPARATOR")); - } else { - var name = part.attribute().orElseThrow(); - attribute = structure.flatMap(st -> findMember(st, name.text())); - recordReference(owner, name, attribute.orElse(null), ReferenceUsage.TYPE_USE); - if (attribute.isEmpty()) errors.add(unresolved(name)); - attribute.flatMap(index::byId).ifPresent(this::attributeType); - TypeDescriptor value = attribute.map(declaredTypes::get).map(this::effectiveDescriptor).orElse(null); - if (part.format().isPresent()) { - nested = bindFormatName(context, part.format().orElseThrow(), Set.of(SymbolKind.DOMAIN)); - TypeDescriptor nestedType = nested.map(declaredTypes::get).map(this::effectiveDescriptor).orElse(null); - SymbolId selected = value instanceof CollectionType c && !isMany(c.cardinality()) ? structures.target(c) : structures.target(value); - if (value instanceof CollectionType c && isMany(c.cardinality())) selected = null; - if (!(nestedType instanceof FormattedType f) || f.structure().isEmpty() || selected == null - || !targetSpecializes(selected, f.structure().orElseThrow())) - errors.add(emitRule(owner, "nested FORMAT requires a compatible singleton structure", "FORMAT_ATTRIBUTE")); - } else if (!(value instanceof NumericType)) errors.add(emitRule(owner, "FORMAT attribute must be numeric", "FORMAT_ATTRIBUTE")); - else { - NumericRange range = ((NumericType)value).range(); - if (range.minimum().isPresent() && range.maximum().isPresent() - && range.minimum().orElseThrow().precision().fractionalDigits() - != range.maximum().orElseThrow().precision().fractionalDigits()) - errors.add(emitRule(owner, "FORMAT component bounds must have matching precision", "FORMAT_PRECISION")); - } - if (part.width().filter(w -> w.signum() <= 0).isPresent()) errors.add(emitRule(owner, "FORMAT width must be positive", "FORMAT_WIDTH")); - } - parts.add(new FormattedType.Part(attribute, part.width(), nested, part.literal(), new Origin.DeclaredOrigin(part.range()))); - } - var result = new FormattedType(id, new Origin.DeclaredOrigin(syntax.range()), - syntax.base().map(guru.interlis.syntax.NameSyntax::text).orElse(""), - predefined, domain, structure, parts, syntax.minimum(), syntax.maximum()); - if (result.minimum().isPresent() || result.maximum().isPresent()) { - var min = result.minimum().flatMap(v -> formatValue(result, v)); - var max = result.maximum().flatMap(v -> formatValue(result, v)); - if (min.isEmpty() || max.isEmpty() || FormattedValues.compare(min.orElseThrow(), max.orElseThrow()) > 0) - errors.add(emitRule(owner, "FORMAT bounds must fit the layout and component ranges", "FORMAT_BOUNDS")); - else { - Set seen = new LinkedHashSet<>(); - for (FormattedType ancestor = base; ancestor != null && seen.add(ancestor.id());) { - checkWork(); - var lower = ancestor.minimum().isPresent() ? formatValue(ancestor, ancestor.minimum().orElseThrow()) : Optional.>empty(); - var upper = ancestor.maximum().isPresent() ? formatValue(ancestor, ancestor.maximum().orElseThrow()) : Optional.>empty(); - if (lower.filter(v -> FormattedValues.compare(min.orElseThrow(), v) < 0).isPresent() - || upper.filter(v -> FormattedValues.compare(max.orElseThrow(), v) > 0).isPresent()) - errors.add(emitRule(owner, "FORMAT extension widens base bounds", "FORMAT_BOUNDS")); - TypeDescriptor next = ancestor.baseDomain().map(declaredTypes::get).map(this::effectiveDescriptor).orElse(null); - ancestor = next instanceof FormattedType f ? f : null; - } - } - } - return errors.isEmpty()?result:new ErrorType(id,new Origin.DeclaredOrigin(syntax.range()),errors); - } - - private void validateFormattedUses() { - for (var entry : declaredTypes.entrySet()) { - checkWork(); - DeclInfo declaration = index.byId(entry.getKey()).orElse(null); - if (declaration == null || declaration.kind() != SymbolKind.DOMAIN && declaration.kind() != SymbolKind.ATTRIBUTE) - continue; - TypeDescriptor type = effectiveDescriptor(entry.getValue()); - while (type instanceof CollectionType collection) type = effectiveDescriptor(collection.elementType()); - if (type instanceof FormattedType format && formattedValues().isAbstract(format) - && !declaration.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) - emitRule(declaration, "FORMAT with abstract components requires an abstract domain or attribute", "FORMAT_ABSTRACT"); - } - } - - private FormattedValues formattedValues() { - return new FormattedValues(id -> effectiveDescriptor(declaredTypes.get(id)), this::checkWork); - } - - private Optional> formatValue(FormattedType type, String value) { - return formattedValues().parse(type, value, false); - } - - private TypeId buildType(TypeSyntax syntax, SymbolId context, SourceId source) { - checkWork(); - Objects.requireNonNull(syntax, "syntax"); - TypeId id = nextTypeId(); - TypeDescriptor descriptor = switch (syntax) { - case TextTypeSyntax text -> { - if (text.maxLength().isPresent() && text.maxLength().getAsInt() == 0) - yield new ErrorType(id, new Origin.DeclaredOrigin(text.range()), List.of(emitRule(index.byId(context).orElseThrow(), - "declared text maximum length must be positive", "TEXT_LENGTH", text.range()))); - yield new TextType(id, new Origin.DeclaredOrigin(text.range()), text.textKind(), - text.maxLength().isPresent() ? text.maxLength() : switch (text.textKind()) { - case NAME -> java.util.OptionalInt.of(255); - case URI -> java.util.OptionalInt.of(1023); - default -> java.util.OptionalInt.empty(); - }); - } - case BooleanTypeSyntax bool -> new guru.interlis.model.BooleanType(id, - new Origin.DeclaredOrigin(bool.range())); - case TemporalTypeSyntax syntaxTime -> { - String name = switch (syntaxTime.temporalKind()) { - case DATE -> "XMLDate"; case TIMEOFDAY -> "XMLTime"; case DATETIME -> "XMLDateTime"; - }; - var domain = temporal.find(index.byId(context).orElseThrow(), "INTERLIS." + name, Set.of(SymbolKind.DOMAIN)).orElseThrow(); - yield new DomainReferenceType(id, new Origin.DeclaredOrigin(syntaxTime.range()), domain); - } - case OidTypeSyntax oid -> { - Optional carrier = oid.valueType().map(t -> buildType(t, context, source)); - if (oid.oidKind() == guru.interlis.model.OidKind.UUID) { - TypeId text = nextTypeId(); types.put(text, new TextType(text, new Origin.DeclaredOrigin(oid.range()), TextKind.TEXT, java.util.OptionalInt.of(36))); - carrier = Optional.of(text); - } - yield new guru.interlis.model.OidType(id, new Origin.DeclaredOrigin(oid.range()), oid.oidKind(), carrier); - } - case BlackboxTypeSyntax blackbox -> new guru.interlis.model.BlackboxType(id, - new Origin.DeclaredOrigin(blackbox.range()), blackbox.blackboxKind()); - case AlignmentTypeSyntax alignment -> new guru.interlis.model.AlignmentType(id, - new Origin.DeclaredOrigin(alignment.range()), alignment.alignment()); - case EnumTreeValueSyntax enumTree -> { - List errors = new ArrayList<>(); - Optional domain = bindTarget(index.byId(context).orElseThrow(), enumTree.target(), - Set.of(SymbolKind.DOMAIN), ReferenceUsage.TYPE_USE, errors); - if (domain.isEmpty()) yield new ErrorType(id, new Origin.DeclaredOrigin(enumTree.range()), errors); - index.byId(domain.orElseThrow()).ifPresent(this::domainType); - TypeDescriptor base=effectiveDescriptor(declaredTypes.get(domain.orElseThrow())); - if(!(base instanceof EnumerationType enumeration) || enumeration.treeValues()) - yield new ErrorType(id,new Origin.DeclaredOrigin(enumTree.range()),List.of(emitRule(index.byId(context).orElseThrow(),"ALL OF requires an enumeration domain","ENUM_TREE_TARGET",enumTree.range()))); - yield new EnumerationType(id,new Origin.DeclaredOrigin(enumTree.range()),enumeration.values(),enumeration.ordered(), - enumeration.circular(),enumeration.finalSubtrees(),Optional.of(enumeration.id()),true); - } - case guru.interlis.syntax.EnumArgumentTypeSyntax argument -> - new guru.interlis.model.EnumArgumentType(id,new Origin.DeclaredOrigin(argument.range()),argument.treeValues()); - - case guru.interlis.syntax.StructureValueTypeSyntax structure -> structures.bind(id,structure,index.byId(context).orElseThrow()); - case GenericTypeSyntax generic -> { - if ("INHERITED".equals(generic.name())) { - yield new ErrorType(id, new Origin.DeclaredOrigin(generic.range()), List.of(emitRule( - index.byId(context).orElseThrow(), "a type can only be omitted on an inherited attribute", - "INHERITED_TYPE_CONTEXT"))); - } - yield new GenericType(id, new Origin.DeclaredOrigin(generic.range()), generic.name()); - } - case guru.interlis.syntax.AttributePathTypeSyntax path -> { - Optional owner = Optional.empty(); - if (path.argument().isPresent()) { - DeclInfo declaration = index.byId(context).orElseThrow(); - String name = path.argument().orElseThrow().text(); - int position = -1; - if (declaration.syntax() instanceof FunctionDeclarationSyntax function) { - for (int i = 0; i < function.parameters().size(); i++) - if (function.parameters().get(i).name().equals(name)) { position = i; break; } - } - if (position < 0) { - yield new ErrorType(id, new Origin.DeclaredOrigin(path.range()), List.of(emitRule(declaration, - "ATTRIBUTE OF @ requires an argument of this function", "ATTRIBUTE_ARGUMENT"))); - } - TypeDescriptor argumentType = effectiveDescriptor(functionArgumentType(declaration, position)); - if (!(argumentType instanceof guru.interlis.model.ObjectArgumentType || argumentType instanceof StructureValueType - || argumentType instanceof ClassValueType || argumentType instanceof guru.interlis.model.AttributePathType || argumentType instanceof ErrorType)) - emitRule(declaration, "ATTRIBUTE OF @ argument must describe objects, classes or an attribute path", "ATTRIBUTE_ARGUMENT_TYPE"); - owner = Optional.of(new guru.interlis.model.AttributePathType.Argument(context, position)); - } - Optional ownerClass = path.ownerClass().map(expression -> buildExpression(expression, index.byId(context).orElseThrow().owner().orElse(context))); - List restrictions = path.restrictions().stream().map(t -> buildType(t, context, source)).toList(); - if (ownerClass.isPresent() && !(effectiveDescriptor(ownerClass.orElseThrow().typeId()) instanceof ClassValueType)) - emitRule(index.byId(context).orElseThrow(), "ATTRIBUTE OF requires a class-valued attribute path", "ATTRIBUTE_PATH_TYPE"); - yield new guru.interlis.model.AttributePathType(id, new Origin.DeclaredOrigin(path.range()), - ownerClass.isPresent() ? Optional.of(new guru.interlis.model.AttributePathType.ClassAttribute(ownerClass.orElseThrow().id())) : owner, restrictions); - } - case ClassValueTypeSyntax classValue -> { - List errors = new ArrayList<>(); - List restrictions = new ArrayList<>(); - var kinds = classValue.structureValue() ? Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE) - : Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION, SymbolKind.VIEW); - for (var name : classValue.restrictions()) { - bindTarget(index.byId(context).orElseThrow(), name, kinds, ReferenceUsage.TYPE_USE, errors).ifPresent(target -> { - SymbolId current = target; - Set visited = new HashSet<>(); - while (current != null && visited.add(current)) { checkWork(); current = objectInheritanceBase(current).orElse(null); } - restrictions.add(target); - }); - } - yield errors.isEmpty() ? new ClassValueType(id, new Origin.DeclaredOrigin(classValue.range()), classValue.structureValue(), restrictions) - : new ErrorType(id, new Origin.DeclaredOrigin(classValue.range()), errors); - } - case guru.interlis.syntax.ObjectArgumentTypeSyntax object -> - buildObjectArgumentType(id, object, context); - case guru.interlis.syntax.MetaObjectTypeSyntax meta -> { - var declaration = index.byId(context).orElseThrow(); - var target = meta.target().flatMap(name -> { - var predefined = metadata.predefined(declaration, name.text(), Set.of(SymbolKind.CLASS)); - if (predefined.isPresent()) recordReference(declaration, name, predefined.get(), ReferenceUsage.TYPE_USE); - return predefined.isPresent() ? predefined : bindTarget(declaration, name, Set.of(SymbolKind.CLASS), ReferenceUsage.TYPE_USE, new ArrayList<>()); - }); - target.ifPresent(idTarget -> { - metadata.ensureBases(idTarget); - var root = metadata.predefined(declaration, "INTERLIS.METAOBJECT", Set.of(SymbolKind.CLASS)).orElseThrow(); - if (!guru.interlis.model.internal.TypeCompatibility.specializes(idTarget, root, baseOf::get)) - emitRule(declaration, "METAOBJECT OF requires a metadata class", "PARAMETER_METAOBJECT_TARGET"); - }); - yield new guru.interlis.model.MetaObjectType(id, new Origin.DeclaredOrigin(meta.range()), target); - } - case LineTypeSyntax line -> lines.buildType(id, line, index.byId(context).orElseThrow()); - case FormattedTypeSyntax formatted -> buildFormattedType(id, formatted, context); - case guru.interlis.syntax.NumericTypeSyntax numeric -> { - var invalid = new ArrayList<>(numericErrors(numeric, index.byId(context).orElseThrow())); - if (!invalid.isEmpty()) yield new ErrorType(id, new Origin.DeclaredOrigin(numeric.range()), invalid); - var range=metadata.numeric(numeric,index.byId(context).orElseThrow(),invalid); - validateNumericUnit(range,index.byId(context).orElseThrow(),numeric.range(),invalid); - yield invalid.isEmpty()?new NumericType(id,new Origin.DeclaredOrigin(numeric.range()),range) - :new ErrorType(id,new Origin.DeclaredOrigin(numeric.range()),invalid); - } - case EnumerationTypeSyntax enumeration -> { - DeclInfo declaration = index.byId(context).orElseThrow(); - TypeDescriptor inherited = null; - if (declaration.kind() == SymbolKind.ATTRIBUTE && declaration.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - SymbolId owner = declaration.owner().orElse(null); - AttributeSymbol baseAttribute = owner == null ? null : findBaseAttribute(baseOf.get(owner), declaration.name()); - if (baseAttribute != null) inherited = effectiveDescriptor(baseAttribute.declaredType()); - } - // Domains are merged after their base reference has been bound. - yield declaration.kind() == SymbolKind.DOMAIN - ? new EnumerationType(id, new Origin.DeclaredOrigin(enumeration.range()), List.of(), - enumeration.ordered(), enumeration.circular(), enumeration.finalSubtrees(), Optional.empty(), false) - : effectiveEnumeration(id, enumeration, inherited, declaration); - } - case DomainReferenceSyntax reference -> { - var declaration=index.byId(context).orElseThrow();var errors=new ArrayList(); - var target=bindTarget(declaration,reference.name(),Set.of(SymbolKind.DOMAIN,SymbolKind.STRUCTURE),ReferenceUsage.TYPE_USE,errors); - if(target.isEmpty())yield new ErrorType(id,new Origin.DeclaredOrigin(reference.range()),errors); - SymbolId selected=target.orElseThrow(); - var sourceDeclaration=index.byId(selected); - SymbolKind kind=sourceDeclaration.map(DeclInfo::kind).orElseGet(()->symbols.get(selected).kind()); - if(kind==SymbolKind.STRUCTURE)yield new StructureValueType(id,new Origin.DeclaredOrigin(reference.range()),Optional.of(selected),List.of()); - sourceDeclaration.ifPresent(this::domainType); - yield new DomainReferenceType(id,new Origin.DeclaredOrigin(reference.range()),selected); - } - case ReferenceTypeSyntax reference -> { - DeclInfo declaration = index.byId(context).orElseThrow(); - Optional target = reference.target().text().equals("ANYCLASS") ? Optional.empty() - : Optional.of(bindObjectTarget(declaration, reference.target(), ReferenceUsage.TYPE_USE)); - List restrictions = reference.restrictions().stream() - .map(name -> bindObjectTarget(declaration, name, ReferenceUsage.TYPE_USE)).toList(); - for (SymbolId restriction : restrictions) { - checkWork(); - if (target.isPresent() && !targetSpecializes(restriction, target.orElseThrow())) - emitRule(declaration, "reference restriction must specialize its primary target", "REFERENCE_TARGET_RESTRICTION"); - } - yield new ReferenceType(id, new Origin.DeclaredOrigin(reference.range()), target, restrictions, reference.external()); - } - case CollectionTypeSyntax collection -> { - TypeId element = buildType(collection.elementType(), context, source); - if (index.byId(context).orElseThrow().language() == guru.interlis.model.LanguageVersion.ILI23 - && !(effectiveDescriptor(element) instanceof StructureValueType) - && !(effectiveDescriptor(element) instanceof ErrorType)) - emitRule(index.byId(context).orElseThrow(), "INTERLIS 2.3 collections require structure elements", "COLLECTION_ELEMENT_VERSION"); - yield new CollectionType(id, new Origin.DeclaredOrigin(collection.range()), - collection.collectionKind(), collection.cardinality(), element); - } - case CoordinateTypeSyntax coordinate -> { - List invalid = new ArrayList<>(); - for (var axis : coordinate.axes()) invalid.addAll(numericErrors(axis, index.byId(context).orElseThrow())); - if (!invalid.isEmpty()) yield new ErrorType(id, new Origin.DeclaredOrigin(coordinate.range()), invalid); - var axes=new ArrayList(); - for(var axis:coordinate.axes()) { - var range=metadata.numeric(axis,index.byId(context).orElseThrow(),invalid); - validateNumericUnit(range,index.byId(context).orElseThrow(),axis.range(),invalid);axes.add(range); - } - yield invalid.isEmpty()?new guru.interlis.model.CoordinateType(id, - new Origin.DeclaredOrigin(coordinate.range()),coordinate.dimensions(),axes,coordinate.multiCoordinate(),coordinate.referenceSystem(),coordinate.rotation()) - :new ErrorType(id,new Origin.DeclaredOrigin(coordinate.range()),invalid); - } - case guru.interlis.syntax.UnsupportedTypeSyntax unsupported -> { - if (unsupported.featureId().equals("MISSING_ATTRIBUTE_TYPE") || unsupported.featureId().equals("MISSING_FUNCTION_RETURN_TYPE")) { - DiagnosticId cause = null; - int distance = Integer.MAX_VALUE; - for (var diagnostic : syntaxErrors) { - checkWork(); - var range = diagnostic.primaryRange().orElse(null); - if (range == null || !range.source().equals(unsupported.range().source()) - || !range.revision().equals(unsupported.range().revision())) continue; - int candidate = Math.abs(range.startOffset() - unsupported.range().endOffset()); - if (candidate < distance) { cause = diagnostic.id(); distance = candidate; } - } - if (cause == null) cause = emitRule(index.byId(context).orElseThrow(), "attribute type is missing", "SOURCE_MISSING_TYPE", unsupported.range()); - yield new ErrorType(id, new Origin.DeclaredOrigin(unsupported.range()), List.of(cause)); - } - DiagnosticId diagnostic = emitUnsupported(source, unsupported.range(), - unsupported.featureId(), unsupported.construct()); - yield new ErrorType(id, new Origin.DeclaredOrigin(unsupported.range()), - List.of(diagnostic)); - } - }; - types.put(id, descriptor); - return id; - } - - private TypeDescriptor buildObjectArgumentType(TypeId id, - guru.interlis.syntax.ObjectArgumentTypeSyntax syntax, SymbolId context) { - DeclInfo owner = index.byId(context).orElseThrow(); - List errors = new ArrayList<>(); - List restrictions = new ArrayList<>(); - Optional base = syntax.base().flatMap(name -> - bindObjectTarget(owner, name, errors)); - for (var name : syntax.restrictions()) { - checkWork(); - Optional restriction = bindObjectTarget(owner, name, errors); - restriction.ifPresent(restrictions::add); - if (base.isPresent() && restriction.isPresent() - && !guru.interlis.model.internal.TypeCompatibility.specializes( - restriction.get(), base.get(), baseOf::get)) { - errors.add(collector.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0005"), - Severity.ERROR, Phase.TYPE_CHECKING, "diagnostic.semantics.type") - .at(name.range()) - .argument(new DiagnosticArgument.TypeNameArgument("actual", name.text())) - .argument(new DiagnosticArgument.TypeNameArgument("expected", - "specialization of " + syntax.base().orElseThrow().text())))); - } - } - Origin origin = new Origin.DeclaredOrigin(syntax.range()); - return errors.isEmpty() - ? new guru.interlis.model.ObjectArgumentType(id, origin, base, restrictions, syntax.objects()) - : new ErrorType(id, origin, errors); - } - - private Optional bindObjectTarget(DeclInfo owner, - guru.interlis.syntax.NameSyntax name, List errors) { - return bindTarget(owner, name, Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION, SymbolKind.VIEW), ReferenceUsage.TYPE_USE, errors); - } - - /** One source-name selection for user declarations and canonical predefined targets. */ - private NameResolver.Resolution resolveName(DeclInfo owner, guru.interlis.syntax.NameSyntax name, - Set kinds, boolean relativeQualifiers) { - checkWork(); - if(name.text().startsWith("INTERLIS.")) { - var predefined=predefinedTarget(owner,name.text(),kinds); - if(predefined.isPresent())return new NameResolver.Resolution(NameResolver.Status.BOUND,predefined,List.of()); - return resolver.resolve(owner.id(),name,kinds,relativeQualifiers); - } - if(name.segments().size()!=1)return resolver.resolve(owner.id(),name,kinds,relativeQualifiers); - var local=resolver.resolveLocal(owner.id(),name,kinds); - if(local.status()!=NameResolver.Status.UNRESOLVED)return local; - var normal=resolver.resolve(owner.id(),name,kinds,relativeQualifiers); - boolean visible=index.modelOfSymbol(owner.id()).flatMap(index::byId).filter(d->d.syntax() instanceof guru.interlis.syntax.ModelSyntax) - .map(d->((guru.interlis.syntax.ModelSyntax)d.syntax()).imports().stream().anyMatch(i->i.modelName().equals("INTERLIS") && i.unqualified())).orElse(false); - // Preserve the existing priority of actual imported declarations. In particular, - // ambiguity is a result, never a reason to substitute a builtin function. - if(!visible || normal.status()!=NameResolver.Status.UNRESOLVED)return normal; - var predefined=predefinedTarget(owner,"INTERLIS."+name.text(),kinds); - return predefined.isPresent() - ? new NameResolver.Resolution(NameResolver.Status.BOUND,predefined,List.of()) : normal; - } - - private Optional predefinedTarget(DeclInfo owner,String qualified,Set kinds) { - if(!qualified.startsWith("INTERLIS."))return Optional.empty(); - Optional target=kinds.contains(SymbolKind.DOMAIN)?builtinOidDomain(owner.id(),qualified):Optional.empty(); - if(target.isEmpty() && kinds.contains(SymbolKind.DOMAIN))target=builtinSimpleDomain(owner,qualified); - if(target.isEmpty())target=temporal.find(owner,qualified,kinds); - if(target.isEmpty())target=metadata.predefined(owner,qualified,kinds); - if(target.isEmpty())target=lines.predefined(owner,qualified,kinds); - if(target.isEmpty() && kinds.contains(SymbolKind.UNIT))target=builtinUnit(owner.id(),qualified); - if(target.isEmpty() && kinds.contains(SymbolKind.FUNCTION) && qualified.indexOf('.',9)<0) { - var signature=BuiltinFunctions.find(qualified.substring(9)).filter(f->f.since()!=guru.interlis.model.LanguageVersion.ILI24 || owner.language()==guru.interlis.model.LanguageVersion.ILI24); - if(signature.isPresent())target=Optional.of(builtinDeclaration(signature.orElseThrow(),owner.language()).id()); - } - return target; - } - - Optional bindTarget(DeclInfo owner, guru.interlis.syntax.NameSyntax name, - Set kinds, ReferenceUsage usage, List errors) { - return bindTarget(owner, name, kinds, usage, errors, true); - } - - Optional bindTarget(DeclInfo owner, guru.interlis.syntax.NameSyntax name, - Set kinds, ReferenceUsage usage, List errors, boolean relativeQualifiers) { - var resolution = resolveName(owner, name, kinds, relativeQualifiers); - Optional target = resolution.target(); - recordReference(owner, name, target.orElse(null), usage); - if (resolution.status() == NameResolver.Status.AMBIGUOUS) { - errors.add(collector.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0002"), - Severity.ERROR, Phase.BINDING, "diagnostic.semantics.ambiguous") - .at(name.range()) - .argument(new DiagnosticArgument.IdentifierArgument("name", name.text())) - .argument(new DiagnosticArgument.IdentifierListArgument("candidates", - resolution.candidates().stream().map(SymbolId::value).toList())))); - } else if (target.isEmpty()) { - errors.add(unresolved(name)); - } - return target; - } - - private Optional tryResolve(DeclInfo context, - guru.interlis.syntax.NameSyntax name, - Set expected) { - NameResolver.Resolution resolution = resolver.resolve(context.id(), name, expected); - return resolution.status() == NameResolver.Status.BOUND - ? resolution.target() : Optional.empty(); - } - - DiagnosticId unresolved(guru.interlis.syntax.NameSyntax name) { - var draft = DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0001"), - Severity.ERROR, Phase.BINDING, "diagnostic.semantics.unresolved") - .at(name.range()) - .argument(new DiagnosticArgument.IdentifierArgument("name", name.text())); - // Only qualified references to an actually failed import are dependent. - // An unrelated unresolved name in the same file remains independently visible. - if (name.segments().size() > 1) { - String qualifier = name.segments().getFirst().text(); - closure.models().stream().filter(m -> m.sourceId().equals(name.range().source()) - && m.syntax().range().startOffset() <= name.range().startOffset() - && m.syntax().range().endOffset() >= name.range().endOffset()) - .flatMap(m -> m.syntax().imports().stream()).filter(i -> i.modelName().equals(qualifier)) - .map(i -> collector.importFailure(i.range())).flatMap(Optional::stream).findFirst() - .ifPresent(id -> { draft.cause(id); draft.dependsOn(id); }); - } - return collector.emit(draft); - } - - private TypeId errorType(SourceId source, SourceRange range, String feature) { - TypeId id = nextTypeId(); - DiagnosticId diagnostic = emitUnsupported(source, range, feature, feature); - types.put(id, new ErrorType(id, new Origin.SyntheticOrigin( - guru.interlis.model.SyntheticReason.MISSING_TYPE, Optional.of(range)), - List.of(diagnostic))); - return id; - } - - DiagnosticId emitUnsupported(SourceId source, SourceRange range, String feature, - String construct) { - DiagnosticDraft draft = DiagnosticDraft.of(new DiagnosticCode("ILI-IMPL-0001"), - Severity.ERROR, Phase.SEMANTIC_CHECKING, - "diagnostic.implementation.unsupported") - .argument(new DiagnosticArgument.TextArgument("feature", feature)) - .argument(new DiagnosticArgument.TextArgument("construct", - construct == null ? feature : construct)); - if (range != null) { - draft.at(range); - } - unsupportedBySource.computeIfAbsent(source, ignored -> new LinkedHashSet<>()).add(feature); - return collector.emit(draft); - } - - private TypeId builtinBoolean(guru.interlis.model.LanguageVersion language) { - return booleanTypes.computeIfAbsent(language,version->{ - TypeId id=nextTypeId();types.put(id,new guru.interlis.model.BooleanType(id,new Origin.PredefinedOrigin(version,"BOOLEAN")));return id; - }); - } - - TypeId nextTypeId() { - return new TypeId("type-" + (++typeCounter)); - } - - // ---- expressions ----------------------------------------------------------- - - ExpressionResult buildExpression(ExpressionSyntax syntax, SymbolId owner) { - ExpressionId id = new ExpressionId("expr-" + (++expressionCounter)); - return switch (syntax) { - case LiteralExpressionSyntax literal -> literalExpression(id, literal, owner); - case PathExpressionSyntax path -> pathExpression(id, path, owner); - case BinaryExpressionSyntax binary -> binaryExpression(id, binary, owner); - case UnaryExpressionSyntax unary -> unaryExpression(id, unary, owner); - case DefinedExpressionSyntax defined -> definedExpression(id, defined, owner); - case ConstantExpressionSyntax constant -> constantExpression(id, constant, owner); - case UndefinedExpressionSyntax undefined -> undefinedExpression(id, undefined, owner); - case ParenthesizedExpressionSyntax parenthesized -> - buildExpression(parenthesized.inner(), owner); - case FunctionCallExpressionSyntax call -> functionCallExpression(id, call, owner); - case BuiltinFunctionExpressionSyntax builtin -> builtinFunctionExpression(id, builtin, owner); - case EnumerationConstantExpressionSyntax constant -> enumerationConstantExpression(id, constant, owner); - case ClassConstantExpressionSyntax constant -> classConstantExpression(id, constant, owner); - case guru.interlis.syntax.ObjectSetExpressionSyntax set -> objectSetExpression(id, set, owner); - case guru.interlis.syntax.InspectionExpressionSyntax inspection -> views.factor(id, inspection, owner); - case GenericConstantExpressionSyntax constant -> genericConstantExpression(id, constant, owner); - case guru.interlis.syntax.ParameterExpressionSyntax parameter -> parameterExpression(id, parameter, owner); - case guru.interlis.syntax.AttributeConstantExpressionSyntax constant -> attributeConstantExpression(id, constant, owner); - case UnsupportedExpressionSyntax unsupported -> { - DiagnosticId diagnostic = emitUnsupported(ownerSource(owner), unsupported.range(), - unsupported.featureId(), unsupported.construct()); - TypeId error = errorType(ownerSource(owner), unsupported.range(), - unsupported.featureId()); - ErrorExpression expression = new ErrorExpression(id, error, - guru.interlis.model.ExpressionTypeState.ERROR, unsupported.range(), - List.of(), List.of(diagnostic)); - expressions.put(id, expression); - yield new ExpressionResult(id, error, true); - } - }; - } - - private ExpressionResult objectSetExpression(ExpressionId id, - guru.interlis.syntax.ObjectSetExpressionSyntax syntax, SymbolId owner) { - TypeId type; - if (syntax.selection().isPresent()) { - type = buildType(syntax.selection().orElseThrow(), owner, ownerSource(owner)); - } else { - SymbolId context = expressionContext(owner); - var kind = index.byId(context).map(DeclInfo::kind).orElse(null); - type = nextTypeId(); - if (kind == SymbolKind.CLASS || kind == SymbolKind.ASSOCIATION || kind == SymbolKind.VIEW) { - types.put(type, new guru.interlis.model.ObjectArgumentType(type, - new Origin.DeclaredOrigin(syntax.range()), Optional.of(context), List.of(), true)); - } else { - type = errorType(ownerSource(owner), syntax.range(), "ALL requires an object context"); - } - } - boolean error = effectiveDescriptor(type) instanceof ErrorType; - expressions.put(id, new guru.interlis.model.ObjectSetExpression(id, type, - error ? guru.interlis.model.ExpressionTypeState.ERROR : guru.interlis.model.ExpressionTypeState.RESOLVED, - syntax.range())); - return new ExpressionResult(id, type, error); - } - - private SymbolId expressionContext(SymbolId owner) { - SymbolId context = owner; - while (index.byId(context).map(DeclInfo::kind).orElse(null) == SymbolKind.CONSTRAINT) { - checkWork(); - context = externalConstraintTargets.getOrDefault(context, - index.byId(context).orElseThrow().owner().orElseThrow()); - } - return graphics.context(context); - } - - private ExpressionResult genericConstantExpression(ExpressionId id, - GenericConstantExpressionSyntax syntax, - SymbolId owner) { - TypeId type = nextTypeId(); - types.put(type, new GenericType(type, new Origin.DeclaredOrigin(syntax.range()), syntax.name())); - expressions.put(id, new guru.interlis.model.ConstantExpression(id, type, - guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), - syntax.name())); - return new ExpressionResult(id, type, false); - } - - private ExpressionResult classConstantExpression(ExpressionId id, - ClassConstantExpressionSyntax syntax, - SymbolId owner) { - List errors = new ArrayList<>(); - var declaration = index.byId(owner).orElseThrow(); - var kinds = Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE, SymbolKind.ASSOCIATION, SymbolKind.VIEW); - var target = lines.predefined(declaration, syntax.target().text(), kinds); - if (target.isEmpty()) target = metadata.predefined(declaration, syntax.target().text(), kinds); - if (target.isEmpty()) target = bindTarget(declaration, syntax.target(), kinds, ReferenceUsage.EXPRESSION, errors); - else recordReference(declaration, syntax.target(), target.orElseThrow(), ReferenceUsage.EXPRESSION); - if (target.isEmpty()) return failedValueExpression(id, syntax.range(), errors); - TypeId type = nextTypeId(); - boolean structure = symbols.get(target.orElseThrow()) instanceof StructureSymbol - || index.byId(target.orElseThrow()).map(d -> d.kind() == SymbolKind.STRUCTURE).orElse(false); - types.put(type, new ClassValueType(type, new Origin.DeclaredOrigin(syntax.range()), structure, target.stream().toList())); - expressions.put(id, new guru.interlis.model.SymbolValueExpression(id, type, - guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), target.orElseThrow())); - return new ExpressionResult(id, type, false); - } - - private ExpressionResult attributeConstantExpression(ExpressionId id, guru.interlis.syntax.AttributeConstantExpressionSyntax syntax, SymbolId owner) { - return attributeConstantExpression(id,syntax,owner,expressionContext(owner)); - } - - ExpressionResult contextualAttributeConstant(guru.interlis.syntax.AttributeConstantExpressionSyntax syntax,SymbolId owner,SymbolId context) { - return attributeConstantExpression(new ExpressionId("expr-"+(++expressionCounter)),syntax,owner,context); - } - - private ExpressionResult attributeConstantExpression(ExpressionId id, guru.interlis.syntax.AttributeConstantExpressionSyntax syntax, SymbolId owner,SymbolId context) { - var declaration = index.byId(owner).orElseThrow(); - List errors = new ArrayList<>(); - Optional viewable = syntax.viewable().isPresent() - ? bindTarget(declaration, syntax.viewable().orElseThrow(), Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE, SymbolKind.ASSOCIATION, SymbolKind.VIEW), ReferenceUsage.EXPRESSION, errors) - : Optional.of(context); - Optional attribute = viewable.flatMap(target -> findMember(target, syntax.attribute().text())) - .filter(target -> index.byId(target).map(DeclInfo::kind).orElse(null) == SymbolKind.ATTRIBUTE || symbols.get(target) instanceof AttributeSymbol); - recordReference(declaration, syntax.attribute(), attribute.orElse(null), ReferenceUsage.EXPRESSION); - if (attribute.isEmpty()) { - errors.add(unresolved(syntax.attribute())); - return failedValueExpression(id, syntax.range(), errors); - } - TypeId attributeType = index.byId(attribute.orElseThrow()).map(this::attributeType) - .orElseGet(() -> ((AttributeSymbol) symbols.get(attribute.orElseThrow())).declaredType()); - TypeId type = nextTypeId(); - types.put(type, new guru.interlis.model.AttributePathType(type, new Origin.DeclaredOrigin(syntax.range()), - Optional.of(new guru.interlis.model.AttributePathType.Viewable(viewable.orElseThrow())), List.of(attributeType))); - expressions.put(id, new guru.interlis.model.SymbolValueExpression(id, type, guru.interlis.model.ExpressionTypeState.RESOLVED, - syntax.range(), attribute.orElseThrow())); - return new ExpressionResult(id, type, false); - } - - private ExpressionResult parameterExpression(ExpressionId id, guru.interlis.syntax.ParameterExpressionSyntax syntax, SymbolId owner) { - List errors = new ArrayList<>(); - var target = bindTarget(index.byId(owner).orElseThrow(), syntax.parameter(), Set.of(SymbolKind.PARAMETER), ReferenceUsage.EXPRESSION, errors); - if (target.isEmpty()) return failedValueExpression(id, syntax.range(), errors); - var parameter = (guru.interlis.model.ParameterSymbol) buildSymbol(index.byId(target.orElseThrow()).orElseThrow()); - boolean error = effectiveDescriptor(parameter.typeId()) instanceof ErrorType; - expressions.put(id, new guru.interlis.model.SymbolValueExpression(id, parameter.typeId(), - error ? guru.interlis.model.ExpressionTypeState.ERROR : guru.interlis.model.ExpressionTypeState.RESOLVED, - syntax.range(), parameter.id())); - return new ExpressionResult(id, parameter.typeId(), error); - } - - private ExpressionResult failedValueExpression(ExpressionId id, SourceRange range, List errors) { - TypeId type = nextTypeId(); - types.put(type, new ErrorType(type, new Origin.DeclaredOrigin(range), errors)); - expressions.put(id, new guru.interlis.model.ErrorExpression(id, type, - guru.interlis.model.ExpressionTypeState.ERROR, range, List.of(), errors)); - return new ExpressionResult(id, type, true); - } - - ExpressionResult failedFunctionArgument(SourceRange range,List causes) { - return failedValueExpression(new ExpressionId("expr-"+(++expressionCounter)),range,causes); - } - - ExpressionResult blockedFunctionArgument(SourceRange range,ExpressionResult dependency,DeclInfo owner) { - var descriptor=effectiveDescriptor(dependency.typeId());var source=expressions.get(dependency.id()).range(); - var causes=descriptor instanceof ErrorType error?error.diagnosticIds():collector.snapshot().stream() - .filter(d->d.severity()==Severity.ERROR&&d.primaryRange().filter(r->r.source().equals(source.source())&&r.revision().equals(source.revision()) - &&r.startOffset()<=source.endOffset()&&r.endOffset()>=source.startOffset()).isPresent()).map(guru.interlis.diagnostics.Diagnostic::id).toList(); - if(causes.isEmpty())causes=List.of(emitRule(owner,"invalid actual attribute owner argument","FUNCTION_ATTRIBUTE_DEPENDENCY",range)); - return failedFunctionArgument(range,causes); - } - - private ExpressionResult enumerationConstantExpression(ExpressionId id, - EnumerationConstantExpressionSyntax syntax, - SymbolId owner) { - String text = syntax.text().toLowerCase(java.util.Locale.ROOT); - if (text.equals("#true") || text.equals("#false")) { - TypeId type = builtinBoolean(index.byId(owner).orElseThrow().language()); - expressions.put(id, new guru.interlis.model.LiteralExpression(id, type, - guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), - guru.interlis.model.LiteralKind.BOOLEAN, syntax.text(), Optional.empty(), - Optional.empty())); - return new ExpressionResult(id, type, false); - } - TypeId type = nextTypeId(); - types.put(type,new guru.interlis.model.EnumArgumentType(type,new Origin.DeclaredOrigin(syntax.range()),false)); - expressions.put(id, new guru.interlis.model.ConstantExpression(id, type, - guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), - syntax.text())); - return new ExpressionResult(id, type, false); - } - - private ExpressionResult builtinFunctionExpression(ExpressionId id, - BuiltinFunctionExpressionSyntax syntax, - SymbolId owner) { - var signature = BuiltinFunctions.find(syntax.functionName().segments().getLast().text()).orElse(null); - var declaration = index.byId(owner).orElseThrow(); - boolean available=signature!=null && (signature.since()!=guru.interlis.model.LanguageVersion.ILI24 || declaration.language()==guru.interlis.model.LanguageVersion.ILI24); - List arguments=new ArrayList<>(functions.arguments(available?builtinDeclaration(signature,declaration.language()):null, - syntax.arguments(),declaration,available?signature.rule():BuiltinFunctions.Rule.SIMPLE)); - boolean error = arguments.stream().anyMatch(ExpressionResult::error); - TypeId type; - if (signature == null || signature.since() == guru.interlis.model.LanguageVersion.ILI24 - && declaration.language() != guru.interlis.model.LanguageVersion.ILI24) { - DiagnosticId cause = emitRule(declaration, "Unknown standard function INTERLIS." + syntax.functionName().segments().getLast().text(), - "BUILTIN_NAME"); - type = nextTypeId(); - types.put(type, new ErrorType(type, new Origin.DeclaredOrigin(syntax.range()), List.of(cause))); - error = true; - - } else { - type = builtinResultType(signature.result(), declaration.language()); - if (arguments.size() != signature.arguments().size()) { - emitTypeMismatch(ownerSource(owner), syntax.range(), - arguments.size() + " arguments", signature.arguments().size() + " arguments"); - error = true; - } - for (int i = 0; i < Math.min(arguments.size(), signature.arguments().size()); i++) { - checkWork(); - var actual = arguments.get(i); - var expected = signature.arguments().get(i); - if (!actual.error() && expected!=BuiltinFunctions.Value.ATTRIBUTE - && !(i==1 && (signature.rule()==BuiltinFunctions.Rule.AREAS_ATTRIBUTE || signature.rule()==BuiltinFunctions.Rule.AREAS_TEXT) - && expressions.get(actual.id()) instanceof guru.interlis.model.UndefinedExpression) - && !builtinArgumentMatches(actual.typeId(), expected, declaration.language())) { - emitTypeMismatch(ownerSource(owner), syntax.arguments().get(i).range(), - typeName(actual.typeId()), expected.name()); - error = true; - } - } - } - if(!error && (signature.name().equals("isEnumSubVal") || signature.name().equals("inEnumRange"))) - error=!enumArgumentsValid(arguments,signature,declaration,syntax.range()); - if (!error && syntax.functionName().segments().getLast().text().equals("myClass")) { - TypeDescriptor argument = effectiveDescriptor(arguments.getFirst().typeId()); - List targets = argument instanceof ReferenceType r ? r.targetRoots() - : argument instanceof guru.interlis.model.ObjectArgumentType o ? (o.restrictions().isEmpty() ? o.base().stream().toList() : o.restrictions()) - : argument instanceof StructureValueType structure ? structure.targetRoots() : List.of(); - types.put(type, new ClassValueType(type, new Origin.DeclaredOrigin(syntax.range()), - argument instanceof StructureValueType || guru.interlis.model.internal.TypeCompatibility.isUndefined(argument), targets)); - } - if (!error && signature.rule()==BuiltinFunctions.Rule.CONVERT_UNIT) { - var from=effectiveDescriptor(arguments.getFirst().typeId()); - type=nextTypeId(); - types.put(type,new NumericType(type,new Origin.DeclaredOrigin(syntax.range()),new NumericRange(Optional.empty(),Optional.empty(),false, - guru.interlis.model.NumericDirection.NONE,from instanceof NumericType n?n.range().unit():Optional.empty(),Optional.empty()))); - } - FunctionBindings.Result bound = FunctionBindings.Result.empty(); - if (!error) { - bound = functions.bind(builtinDeclaration(signature,declaration.language()),arguments,declaration,syntax.range(),signature.rule()); - error = bound.error(); - } - if(available)recordReference(declaration,syntax.functionName(), - builtinDeclaration(signature,declaration.language()).id(),ReferenceUsage.EXPRESSION); - expressions.put(id, new guru.interlis.model.FunctionCallExpression(id, type, - error ? guru.interlis.model.ExpressionTypeState.ERROR - : guru.interlis.model.ExpressionTypeState.RESOLVED, - syntax.range(), arguments.stream().map(ExpressionResult::id).toList(), - "INTERLIS." + syntax.functionName().segments().getLast().text(), Optional.ofNullable(available?builtinDeclaration(signature,declaration.language()).id():null),bound.paths())); - return new ExpressionResult(id, type, error); - } - - private boolean enumArgumentsValid(List arguments, BuiltinFunctions.Signature signature, DeclInfo owner, SourceRange range) { - var anchor=arguments.stream().filter(a->!isEnumConstant(a)).map(a->effectiveDescriptor(a.typeId())) - .filter(a->a instanceof EnumerationType || a instanceof guru.interlis.model.BooleanType || a instanceof guru.interlis.model.AlignmentType).findFirst().orElse(null); - if(anchor==null)return true; // Polymorphic literal-only calls have no declared enum identity. - for(int i=0;iv.startsWith(path+"."))); - } else valid=anchor instanceof EnumerationType a && actual instanceof EnumerationType b - && guru.interlis.model.internal.TypeCompatibility.relatedEnumerations(a,b,this::effectiveDescriptor) - || anchor instanceof guru.interlis.model.BooleanType && actual instanceof guru.interlis.model.BooleanType - || anchor instanceof guru.interlis.model.AlignmentType a && actual instanceof guru.interlis.model.AlignmentType b && a.alignment()==b.alignment(); - if(guru.interlis.model.internal.TypeCompatibility.isUndefined(actual))continue; - if(!valid) {emitRule(owner,"enumeration function arguments must refer to compatible values of the same enumeration","ENUM_ARGUMENT",range);return false;} - if(isEnumConstant(argument)) { - TypeId context=anchor.id(); - if(anchor instanceof EnumerationType enumeration && !enumeration.treeValues() - && signature.arguments().get(i)==BuiltinFunctions.Value.ENUM_TREE) { - context=nextTypeId();types.put(context,new EnumerationType(context,enumeration.origin(),enumeration.values(),enumeration.ordered(), - enumeration.circular(),enumeration.finalSubtrees(),Optional.of(enumeration.id()),true)); - } - arguments.set(i,contextualEnumConstant(argument,context,owner.id())); - } - - } - return true; - } - - private List enumerationPaths(TypeDescriptor descriptor) { - if(descriptor instanceof EnumerationType enumeration)return enumeration.values().stream().map(EnumerationValue::path).toList(); - if(descriptor instanceof guru.interlis.model.BooleanType)return List.of("false","true"); - if(descriptor instanceof guru.interlis.model.AlignmentType alignment)return alignment.alignment()==guru.interlis.model.AlignmentKind.HORIZONTAL - ?List.of("Left","Center","Right"):List.of("Top","Cap","Half","Base","Bottom"); - return List.of(); - } - - FunctionSymbol builtinDeclaration(BuiltinFunctions.Signature signature, guru.interlis.model.LanguageVersion language) { - var id=new SymbolId("predefined:"+language+":function:INTERLIS."+signature.name()); - if(symbols.get(id) instanceof FunctionSymbol function)return function; - var parameters=new ArrayList(); - var names=BuiltinFunctions.parameterNames(signature.name()); - for(int i=0;i new NumericType(id, origin, NumericRange.unbounded()); - case CLASS_VALUE -> new ClassValueType(id, origin, true, List.of()); - case POLYLINE, MULTIPOLYLINE, SURFACE, MULTISURFACE -> new GeometryType(id,origin, - guru.interlis.model.GeometryKind.valueOf(value.name()),List.of(),Optional.empty(),Optional.empty(),false,false,Optional.empty()); - case ENUM, ENUM_TREE -> new guru.interlis.model.EnumArgumentType(id,origin,value==BuiltinFunctions.Value.ENUM_TREE); - case OBJECT -> new guru.interlis.model.ObjectArgumentType(id,origin,Optional.empty(),List.of(),false); - case OBJECTS -> new guru.interlis.model.ObjectArgumentType(id, origin, Optional.empty(), List.of(), true); - case ANYSTRUCTURE -> new StructureValueType(id,origin,Optional.empty(),List.of()); - case BAG -> new CollectionType(id,origin,guru.interlis.model.CollectionKind.BAG,Cardinality.unboundedFrom(0), - builtinResultType(BuiltinFunctions.Value.ANYSTRUCTURE,language)); - case TEXT, MTEXT -> new TextType(id, origin, - value == BuiltinFunctions.Value.TEXT ? TextKind.TEXT : TextKind.MTEXT, - java.util.OptionalInt.empty()); - default -> throw new IllegalArgumentException("unsupported builtin result " + value); - }; - types.put(id, descriptor); - return id; - } - - private boolean builtinArgumentMatches(TypeId actual, BuiltinFunctions.Value expected, guru.interlis.model.LanguageVersion language) { - return guru.interlis.model.internal.TypeCompatibility.assign(effectiveDescriptor(actual), - effectiveDescriptor(builtinResultType(expected,language)),this::effectiveDescriptor,baseOf::get, - domain->effectiveDescriptor(declaredTypes.get(domain)),guru.interlis.model.AssignmentContext.FUNCTION_ARGUMENT) - .decision()==guru.interlis.model.RelationDecision.ALLOWED; - } - - private ExpressionResult functionCallExpression(ExpressionId id, - FunctionCallExpressionSyntax syntax, - SymbolId owner) { - var selected=resolveName(index.byId(owner).orElseThrow(),syntax.functionName(),Set.of(SymbolKind.FUNCTION),true); - if(selected.target().map(symbols::get).filter(f->f instanceof FunctionSymbol && f.origin() instanceof Origin.PredefinedOrigin).isPresent()) - return builtinFunctionExpression(id,new BuiltinFunctionExpressionSyntax(syntax.id(),syntax.range(),syntax.functionName(),syntax.arguments()),owner); - List bindingErrors = new ArrayList<>(); - Optional target = bindTarget(index.byId(owner).orElseThrow(), syntax.functionName(), - Set.of(SymbolKind.FUNCTION), ReferenceUsage.EXPRESSION, bindingErrors); - FunctionSymbol function = target.flatMap(index::byId).map(this::buildSymbol) - .filter(FunctionSymbol.class::isInstance).map(FunctionSymbol.class::cast).orElse(null); - List arguments=new ArrayList<>(functions.arguments(function,syntax.arguments(),index.byId(owner).orElseThrow(),BuiltinFunctions.Rule.SIMPLE)); - List children = arguments.stream().map(ExpressionResult::id).toList(); - boolean error = arguments.stream().anyMatch(ExpressionResult::error); - TypeId type; - if (function == null) { - checkWork(); - type = nextTypeId(); - types.put(type, new ErrorType(type, new Origin.DeclaredOrigin(syntax.range()), - bindingErrors)); - error = true; - } else { - type = function.returnType(); - if (arguments.size() != function.parameters().size()) { - emitTypeMismatch(ownerSource(owner), syntax.range(), - arguments.size() + " arguments", function.parameters().size() + " arguments"); - error = true; - } - for (int i = 0; i < Math.min(arguments.size(), function.parameters().size()); i++) { - checkWork(); - var actual = arguments.get(i); - TypeId expected = function.parameters().get(i).typeId(); - actual=contextualValue(actual,expected,owner);arguments.set(i,actual); - if (!actual.error() && !(effectiveDescriptor(expected) instanceof guru.interlis.model.AttributePathType) && guru.interlis.model.internal.TypeCompatibility.assign( - effectiveDescriptor(actual.typeId()), effectiveDescriptor(expected), - this::effectiveDescriptor, baseOf::get, domain -> effectiveDescriptor(declaredTypes.get(domain)),guru.interlis.model.AssignmentContext.FUNCTION_ARGUMENT).decision() - != guru.interlis.model.RelationDecision.ALLOWED) { - emitTypeMismatch(ownerSource(owner), syntax.arguments().get(i).range(), - typeName(actual.typeId()), typeName(expected)); - error = true; - } - } - } - if (!error) error = functions.bind(function,arguments,index.byId(owner).orElseThrow(),syntax.range(),BuiltinFunctions.Rule.SIMPLE).error(); - if (!error) type=functions.returnType(function,arguments,syntax.range()); - expressions.put(id, new guru.interlis.model.FunctionCallExpression(id, type, - error ? guru.interlis.model.ExpressionTypeState.ERROR - : guru.interlis.model.ExpressionTypeState.RESOLVED, - syntax.range(), children, syntax.functionName().text(), target,Map.of())); - return new ExpressionResult(id, type, error); - } - - private ExpressionResult literalExpression(ExpressionId id, LiteralExpressionSyntax syntax, SymbolId owner) { - TypeId type; - TypedExpression expression; - if (syntax.literalKind() == guru.interlis.model.LiteralKind.TEXT) { - String value = unquote(syntax.lexeme()); - type = nextTypeId(); - types.put(type, new TextType(type, new Origin.DeclaredOrigin(syntax.range()), - value.indexOf('\r')>=0 || value.indexOf('\n')>=0 || value.indexOf('\t')>=0 ? TextKind.MTEXT : TextKind.TEXT, - java.util.OptionalInt.of(TextLengths.canonicalLength(value)))); - expression = new guru.interlis.model.LiteralExpression(id, type, - guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), - syntax.literalKind(), syntax.lexeme(), Optional.empty(), - Optional.of(value)); - } else { - type = numericLiteralType(syntax.decimal(), syntax.unit(), syntax.range(), owner); - expression = new guru.interlis.model.LiteralExpression(id, type, - effectiveDescriptor(type) instanceof ErrorType ? guru.interlis.model.ExpressionTypeState.ERROR : guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), - syntax.literalKind(), syntax.lexeme(), syntax.decimal(), Optional.empty()); - } - expressions.put(id, expression); - return new ExpressionResult(id, type, effectiveDescriptor(type) instanceof ErrorType); - } - - private TypeId numericLiteralType(Optional decimal, Optional unitName, SourceRange sourceRange, SymbolId owner) { - TypeId type = nextTypeId(); - Optional unit = unitName.flatMap(name -> bindUnit(index.byId(owner).orElseThrow(), name)); - if (unitName.isPresent() && unit.isEmpty()) { - var diagnostic = emitRule(index.byId(owner).orElseThrow(), "Numeric constant requires a known unit", "CONSTANT_UNIT", unitName.orElseThrow().range()); - types.put(type, new ErrorType(type, new Origin.DeclaredOrigin(sourceRange), List.of(diagnostic))); - return type; - } - if (unit.isPresent() && extensionUnit(unit.orElseThrow()).abstractUnit()) { - var diagnostic = emitRule(index.byId(owner).orElseThrow(), "Numeric constant requires a concrete unit", "CONSTANT_UNIT", unitName.orElseThrow().range()); - types.put(type, new ErrorType(type, new Origin.DeclaredOrigin(sourceRange), List.of(diagnostic))); - return type; - } - NumericRange range = new NumericRange(decimal, decimal, false, - guru.interlis.model.NumericDirection.NONE, unit, Optional.empty()); - types.put(type, new NumericType(type, new Origin.DeclaredOrigin(sourceRange), range)); - return type; - } - - private ExpressionResult pathExpression(ExpressionId id, PathExpressionSyntax syntax, - SymbolId owner) { - if (isSetCondition(owner, syntax.range())) { - checkWork(); - var name=syntax.segments().getFirst().name(); - var diagnostic=collector.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0008"),Severity.ERROR, - Phase.TYPE_CHECKING,"diagnostic.semantics.path") - .at(name.range()).argument(new DiagnosticArgument.IdentifierArgument("segment",name.text())) - .argument(new DiagnosticArgument.TextArgument("reason","SET condition has no individual object context; object values belong in WHERE"))); - return failedValueExpression(id,syntax.range(),List.of(diagnostic)); - } - return pathExpression(id, syntax, owner, expressionContext(owner), false); - } - - /** The source-bound main expression distinguishes SET's set context from - * its individual WHERE context, including nested calls and external blocks. - * Explicit class/attribute constants and separately bound function string - * paths do not enter this ordinary object-value path entry point. */ - private boolean isSetCondition(SymbolId owner,SourceRange range) { - var declaration=index.byId(owner).orElseThrow().syntax(); - var constraint=declaration instanceof ExternalConstraintSyntax external?external.constraint() - :declaration instanceof ConstraintDeclarationSyntax local?local:null; - if(constraint==null||constraint.constraintKind()!=guru.interlis.syntax.ConstraintKind.SET)return false; - return constraint.expression().map(ExpressionSyntax::range).filter(body->body.source().equals(range.source()) - &&body.revision().equals(range.revision())&&body.startOffset()<=range.startOffset()&&body.endOffset()>=range.endOffset()).isPresent(); - } - - private ExpressionResult pathExpression(ExpressionId id, PathExpressionSyntax syntax, - SymbolId owner, SymbolId start, boolean localPrefix) { - return pathExpression(id, syntax, owner, start, localPrefix, PathContext.VALUE); - } - - guru.interlis.syntax.FragmentSnapshot parsePathLiteral(SourceRange range, guru.interlis.model.LanguageVersion language) { - var fragment = new guru.interlis.compiler.internal.antlr.AntlrSyntaxService().parsePathLiteral( - sourceSet.sources().get(range.source()), range, language, cancellation); - collector.importAll(fragment.diagnostics()); - return fragment; - } - - ExpressionResult valuePath(PathExpressionSyntax syntax, SymbolId owner, SymbolId start) { - return pathExpression(new ExpressionId("expr-" + (++expressionCounter)), syntax, owner, start, false, PathContext.FUNCTION_PATH); - } - - ExpressionResult inspectionPath(PathExpressionSyntax syntax, SymbolId owner, SymbolId start) { - return pathExpression(new ExpressionId("expr-" + (++expressionCounter)), syntax, owner, start, false, PathContext.INSPECTION); - } - - private enum PathContext { VALUE, INSPECTION, FUNCTION_PATH } - - private ExpressionResult pathExpression(ExpressionId id, PathExpressionSyntax syntax, - SymbolId owner, SymbolId start, boolean localPrefix, PathContext pathContext) { - boolean inspection=pathContext==PathContext.INSPECTION; - List steps = new ArrayList<>(); - SymbolId currentScope = start; - SymbolId contextTopic = scopeRoot(graphics.context(owner)); - currentScope = contextualObject(currentScope, contextTopic); - TypeId currentType = null; - boolean error = false; - for (int i = 0; i < syntax.segments().size(); i++) { - checkWork(); - var segment = syntax.segments().get(i); - guru.interlis.syntax.NameSyntax name = segment.name(); - String text = name.text(); - if (Set.of("PARENT", "THISAREA", "THATAREA").contains(text)) { - var inspected = views.inspection(expressionContext(owner)); - boolean valid = inspected != null && (text.equals("PARENT") != inspected.area()) - && segment.index().isEmpty() && !segment.associationAccess() && segment.associationQualifier().isEmpty() - && (i == 0 || java.util.Objects.equals(currentScope, inspected.element()) - || java.util.Objects.equals(currentScope, expressionContext(owner))); - if (!valid) { - var diagnostic = emitRule(index.byId(owner).orElseThrow(), "invalid inspection path context", "VIEW_INSPECTION_CONTEXT"); - currentType = nextTypeId(); - types.put(currentType, new ErrorType(currentType, new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); - steps.add(new guru.interlis.model.PathStep.UnresolvedStep(text, name.range())); - error = true; break; - } - currentScope = inspected.parent(); currentType = objectPathType(currentScope, name.range()); - steps.add(new guru.interlis.model.PathStep.InspectionStep(guru.interlis.model.PathStep.InspectionKind.valueOf(text), - expressionContext(owner), currentScope, name.range())); - continue; - } - if ("THIS".equals(text) && segment.index().isEmpty() && !segment.associationAccess() && (currentScope != null || currentType != null - && effectiveDescriptor(currentType) instanceof guru.interlis.model.ObjectArgumentType)) { - steps.add(new guru.interlis.model.PathStep.ThisStep(name.range())); - currentType = i == 0 ? declaredTypes.get(currentScope) : currentType; - if (currentType == null) { - currentType = objectPathType(currentScope, name.range()); - } - TypeDescriptor descriptor = effectiveDescriptor(currentType); - if (!(descriptor instanceof ReferenceType) - && !(descriptor instanceof StructureValueType) - && !(descriptor instanceof guru.interlis.model.ObjectArgumentType)) { - currentScope = null; - } - continue; - } - boolean incomingMany = currentType != null && effectiveDescriptor(currentType) - instanceof guru.interlis.model.ObjectArgumentType set && set.objects(); - Optional member = currentScope == null || segment.associationAccess() || segment.associationQualifier().isPresent() - ? Optional.empty() : findMember(currentScope, text); - // Projections retain role access to their source object, including - // roles which ALL OF does not copy as transfer attributes. - if (i == 0 && member.isEmpty()) { - var projection = views.projectionBase(currentScope); - if (projection != null) { - currentScope = projection.target(); - steps.add(new guru.interlis.model.PathStep.ViewBaseStep(projection.id(), currentScope, name.range())); - if (!segment.associationAccess() && segment.associationQualifier().isEmpty()) - member = findMember(currentScope, text).filter(candidate -> - index.byId(candidate).map(d -> d.kind() == SymbolKind.ROLE).orElse(false)); - } - } - List objectScopes = currentScope == null ? List.of() : List.of(currentScope); - if (currentType != null && effectiveDescriptor(currentType) instanceof guru.interlis.model.ObjectArgumentType objects) { - if (!objects.restrictions().isEmpty()) objectScopes = objects.restrictions(); - else if (objects.base().isPresent()) objectScopes = List.of(objects.base().orElseThrow()); - } - if (!localPrefix && segment.index().isEmpty() && member.isEmpty() && !objectScopes.isEmpty()) { - boolean outgoing = segment.associationQualifier().isPresent(); - List accesses = associationAccesses(objectScopes, contextTopic, name, i == 0, outgoing); - if (!outgoing && !segment.associationAccess() && accesses.isEmpty()) { - accesses = associationAccesses(objectScopes, contextTopic, name, i == 0, true); - } - if (segment.associationQualifier().isPresent()) { - var qualifier = segment.associationQualifier().orElseThrow(); - var target = resolver.resolve(owner, qualifier, Set.of(SymbolKind.ASSOCIATION)).target(); - if (target.isEmpty()) target = baseTopicMember(owner, qualifier.text(), Set.of(SymbolKind.ASSOCIATION)); - recordReference(index.byId(owner).orElseThrow(), qualifier, target.orElse(null), ReferenceUsage.EXPRESSION); - if (target.isEmpty()) unresolved(qualifier); - SymbolId selected = target.orElse(null); - accesses = accesses.stream().filter(a -> a.association().equals(selected)).toList(); - } - if (accesses.size() == 1) { - RoleAccess access = accesses.getFirst(); - boolean associationObject = segment.associationAccess() || access.target().isEmpty(); - currentScope = associationObject ? access.association() - : commonRoleScope(access.target().orElseThrow(), contextTopic); - Cardinality cardinality = accessCardinality(access, associationObject); - boolean many = incomingMany || isMany(cardinality); - currentType = nextTypeId(); - types.put(currentType, associationObject - ? new guru.interlis.model.ObjectArgumentType(currentType, new Origin.DeclaredOrigin(segment.range()), - Optional.of(currentScope), List.of(), many) - : rolePathType(currentType, access.target().orElseThrow(), contextTopic, segment.range(), many)); - if (access.target().isEmpty()) { - steps.add(new guru.interlis.model.PathStep.AssociationSourceStep(text, access.association(), - access.source().id(), cardinality, currentType, segment.range())); - } else { - steps.add(new guru.interlis.model.PathStep.AssociationAccessStep(text, - access.association(), access.source().id(), access.target().orElseThrow().id(), - associationObject, cardinality, currentType, segment.range())); - } - recordReference(index.byId(owner).orElseThrow(), name, - access.target().orElse(access.source()).id(), ReferenceUsage.EXPRESSION); - continue; - } else if (accesses.size() > 1) { - var diagnostic = collector.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0002"), - Severity.ERROR, Phase.BINDING, "diagnostic.semantics.ambiguous") - .at(name.range()).argument(new DiagnosticArgument.IdentifierArgument("name", text)) - .argument(new DiagnosticArgument.IdentifierListArgument("candidates", - accesses.stream().map(a -> a.association().value() + ":" + a.source().id().value()).toList()))); - currentType = nextTypeId(); - types.put(currentType, new ErrorType(currentType, new Origin.DeclaredOrigin(segment.range()), List.of(diagnostic))); - steps.add(new guru.interlis.model.PathStep.UnresolvedStep(text, segment.range())); - error = true; - break; - } - } - if (member.isEmpty()) { - DiagnosticId diagnostic = unresolved(name); - steps.add(new guru.interlis.model.PathStep.UnresolvedStep(text, name.range())); - TypeId errorType = nextTypeId(); - types.put(errorType, new ErrorType(errorType, - new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); - currentType = errorType; - error = true; - break; - } - var referenceTarget = symbols.get(member.orElseThrow()); - recordReference(index.byId(owner).orElseThrow(), name, - referenceTarget instanceof ViewSymbol.Base basis && basis.nameRange().isEmpty() ? basis.target() : member.orElseThrow(), - ReferenceUsage.EXPRESSION); - SymbolKind kind = index.byId(member.get()).map(DeclInfo::kind).orElse(SymbolKind.ATTRIBUTE); - if ((localPrefix || segment.index().isPresent()) && kind != SymbolKind.ATTRIBUTE) { - DiagnosticId diagnostic = emitTypeMismatch(ownerSource(owner), name.range(), "role", "structure attribute"); - currentType = nextTypeId(); - types.put(currentType, new ErrorType(currentType, new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); - error = true; - break; - } - if (kind == SymbolKind.VIEW_BASE) { - ViewSymbol.Base base = (ViewSymbol.Base) buildSymbol(index.byId(member.get()).orElseThrow()); - steps.add(new guru.interlis.model.PathStep.ViewBaseStep(base.id(), views.basisTarget(base), name.range())); - currentScope = views.basisTarget(base); currentType = objectPathType(currentScope, name.range()); - } else if (kind == SymbolKind.ROLE) { - steps.add(new guru.interlis.model.PathStep.RoleStep(text, member.get(), - name.range())); - // A role declared on this association selects one participant of - // the association instance. Its relationship cardinality does - // not turn that participant into an object set. - RoleSymbol role = (RoleSymbol) buildSymbol(index.byId(member.get()).orElseThrow()); - currentScope = commonRoleScope(role, contextTopic); - currentType = nextTypeId(); - types.put(currentType, rolePathType(currentType, role, contextTopic, name.range(), incomingMany)); - } else { - steps.add(new guru.interlis.model.PathStep.AttributeStep(text, member.get(), - name.range())); - currentType = declaredTypes.get(member.get()); - if (currentType == null) currentType = attributeType(index.byId(member.get()).orElseThrow()); - TypeDescriptor descriptor = currentType == null ? null - : effectiveDescriptor(currentType); - currentScope = null; - if (segment.index().isPresent()) { - var selection = segment.index().orElseThrow(); - TypeId selected = incomingMany ? null : indexedType(descriptor, selection, segment.range()); - if (selected == null) { - DiagnosticId diagnostic = emitTypeMismatch(ownerSource(owner), segment.range(), - typeName(currentType), "valid LIST position or coordinate axis"); - currentType = nextTypeId(); - types.put(currentType, new ErrorType(currentType, new Origin.DeclaredOrigin(segment.range()), List.of(diagnostic))); - error = true; - break; - } - currentType = selected; - descriptor = effectiveDescriptor(currentType); - steps.add(new guru.interlis.model.PathStep.IndexStep(selection, currentType, segment.range())); - } - if (localPrefix) { - currentScope = structures.target(descriptor); - if (currentScope == null) { - DiagnosticId diagnostic = emitTypeMismatch(ownerSource(owner), name.range(), - typeName(currentType), "structure attribute"); - currentType = nextTypeId(); - types.put(currentType, new ErrorType(currentType, new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); - error = true; - break; - } - } else if (descriptor instanceof CollectionType collection && !incomingMany - && (pathContext!=PathContext.VALUE || !isMany(collection.cardinality())) && i + 1 < syntax.segments().size()) { - currentScope = structures.target(collection); - } else if (descriptor instanceof ReferenceType reference) { - List roots = reference.targetRoots().stream().map(t -> contextualObject(t, contextTopic)).toList(); - currentScope = commonObjectScope(roots); - currentType = nextTypeId(); - if (incomingMany) { - types.put(currentType, new guru.interlis.model.ObjectArgumentType(currentType, - new Origin.DeclaredOrigin(name.range()), roots.size() == 1 ? Optional.of(roots.getFirst()) : Optional.empty(), - roots.size() <= 1 ? List.of() : roots, true)); - } else { - types.put(currentType, new ReferenceType(currentType, new Origin.DeclaredOrigin(name.range()), - reference.target().map(t -> contextualObject(t, contextTopic)), - reference.restrictions().stream().map(t -> contextualObject(t, contextTopic)).toList(), reference.external())); - } - } else if (inspection && descriptor instanceof GeometryType geometry && i + 1 < syntax.segments().size()) { - currentScope = views.geometryElement(index.byId(owner).orElseThrow(), geometry, false); - } else if (!incomingMany && descriptor instanceof StructureValueType structure) { - currentScope = structures.target(structure); - } else if (!incomingMany && descriptor instanceof guru.interlis.model.ObjectArgumentType object) { - // Derived view values can retain a participant object type. - // Continue at its actual roots just as for a bound role. - var roots = object.restrictions().isEmpty() ? object.base().stream().toList() : object.restrictions(); - currentScope = commonObjectScope(roots.stream().map(t -> contextualObject(t, contextTopic)).toList()); - } else if (incomingMany) { - DiagnosticId diagnostic = emitTypeMismatch(ownerSource(owner), name.range(), - "multiple attribute values", "single attribute value"); - currentType = nextTypeId(); - types.put(currentType, new ErrorType(currentType, new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); - error = true; - break; - } - } - } - if (currentType == null) { - currentType = errorType(ownerSource(owner), syntax.range(), "PATH"); - error = true; - } - guru.interlis.model.ObjectPath path = new guru.interlis.model.ObjectPath(steps.isEmpty() - ? List.of(new guru.interlis.model.PathStep.ThisStep(syntax.range())) : steps); - TypedExpression expression = new guru.interlis.model.PathExpression(id, currentType, - error ? guru.interlis.model.ExpressionTypeState.ERROR - : guru.interlis.model.ExpressionTypeState.RESOLVED, - syntax.range(), List.of(), path); - expressions.put(id, expression); - return new ExpressionResult(id, currentType, error); - } - - private TypeId indexedType(TypeDescriptor type, guru.interlis.model.PathIndex index, - guru.interlis.source.SourceRange range) { - var position = index.position(); - if (position.filter(p -> p.signum() <= 0).isPresent()) return null; - if (type instanceof CollectionType collection && collection.collectionKind() == guru.interlis.model.CollectionKind.LIST) { - if (collection.cardinality().upper() instanceof guru.interlis.model.CardinalityUpper.Finite maximum - && (maximum.value().signum() == 0 || position.filter(p -> p.compareTo(maximum.value()) > 0).isPresent())) - return null; - return collection.elementType(); - } - if (type instanceof CoordinateType coordinate && !coordinate.multiCoordinate() && position.isPresent() - && position.orElseThrow().compareTo(java.math.BigInteger.valueOf(coordinate.dimensions())) <= 0) { - TypeId result = nextTypeId(); - types.put(result, new NumericType(result, new Origin.DeclaredOrigin(range), - coordinate.axes().get(position.orElseThrow().intValueExact() - 1))); - return result; - } - return null; - } - - private boolean isMany(Cardinality cardinality) { - return !(cardinality.upper() instanceof guru.interlis.model.CardinalityUpper.Finite finite) - || finite.value().compareTo(java.math.BigInteger.ONE) > 0; - } - - private Cardinality accessCardinality(RoleAccess access, boolean associationObject) { - var roles = associationRoles(access.association()); - // A role bound only for one participant does not constrain the remaining - // combinations in an n-ary relation. Never infer a singleton from it. - if (roles.size() != 2) return Cardinality.unboundedFrom(0); - RoleSymbol target = access.target().orElseGet(() -> roles.stream() - .filter(r -> !r.id().equals(access.source().id())).findFirst().orElseThrow()); - Cardinality cardinality = target.cardinality(); - if (associationObject) { - Cardinality links = Cardinality.exactly(1); - for (SymbolId base : baseChain(access.association())) { - var syntax = (AssociationDeclarationSyntax) index.byId(base).orElseThrow().syntax(); - if (syntax.cardinality().isPresent()) { links = syntax.cardinality().orElseThrow(); break; } - } - cardinality = multiplyCardinality(cardinality, links); - } - return cardinality; - } - - private Cardinality multiplyCardinality(Cardinality left, Cardinality right) { - var upper = left.upper() instanceof guru.interlis.model.CardinalityUpper.Finite l - && right.upper() instanceof guru.interlis.model.CardinalityUpper.Finite r - ? new guru.interlis.model.CardinalityUpper.Finite(l.value().multiply(r.value())) - : guru.interlis.model.CardinalityUpper.unbounded(); - return new Cardinality(left.lower().multiply(right.lower()), upper); - } - - private record RoleAccess(SymbolId association, RoleSymbol source, Optional target) { } - - private List associationAccesses(List fromTargets, SymbolId contextTopic, - guru.interlis.syntax.NameSyntax name, boolean first, boolean outgoing) { - List result = new ArrayList<>(); - for (DeclInfo association : index.all()) { - checkWork(); - if (association.kind() != SymbolKind.ASSOCIATION) continue; - SymbolId topic = scopeRoot(association.id()); - // Traversing a role or projecting an object retains the role namespace - // of that reached object, in addition to the declaring context. - if (!topicAvailable(contextTopic, topic) - && !fromTargets.stream().allMatch(from -> topicAvailable(scopeRoot(from), topic))) continue; - // A declaration appearing later cannot retroactively add an access to a constraint. - if (association.sourceId().equals(name.range().source()) - && association.syntax().range().startOffset() > name.range().startOffset()) continue; - List roles = associationRoles(association.id()); - for (RoleSymbol target : roles) { - checkWork(); - if (!outgoing && !target.name().value().equals(name.text())) continue; - for (RoleSymbol source : roles) { - checkWork(); - if (outgoing ? !source.id().equals(target.id()) : source.id().equals(target.id())) continue; - if (outgoing && !source.name().value().equals(name.text())) continue; - if (!fromTargets.stream().allMatch(from -> roleAccepts(source, from))) continue; - if (first) { - if (source.external()) continue; - if (!fromTargets.stream().allMatch(from -> { - boolean originalAccess = source.targetRoots().stream() - .anyMatch(t -> targetSpecializes(from, t) && Objects.equals(scopeRoot(source.id()), scopeRoot(t))); - boolean extendedHere = Objects.equals(topic, scopeRoot(from)) - && index.byId(from).map(d -> d.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)).orElse(false); - return originalAccess || extendedHere; - })) continue; - } - result.add(new RoleAccess(association.id(), source, outgoing ? Optional.empty() : Optional.of(target))); - } - } - } - List candidates = List.copyOf(result); - result.removeIf(candidate -> candidates.stream().anyMatch(other -> { - if (candidate == other) return false; - boolean derived = baseChain(other.association()).contains(candidate.association()); - boolean base = baseChain(candidate.association()).contains(other.association()); - boolean localExtension = index.byId(other.association()).orElseThrow().modifiers() - .contains(guru.interlis.model.Modifier.EXTENDED); - boolean candidateExtension = index.byId(candidate.association()).orElseThrow().modifiers() - .contains(guru.interlis.model.Modifier.EXTENDED); - return derived && (other.source().hiding() || localExtension) - || base && !candidate.source().hiding() && !candidateExtension; - })); - return result; - } - - private boolean topicAvailable(SymbolId context, SymbolId candidate) { - Set seen = new HashSet<>(); - for (SymbolId current = context; current != null && seen.add(current);) { - checkWork(); - if (current.equals(candidate)) return true; - DeclInfo declaration = index.byId(current).orElse(null); - if (declaration == null || !(declaration.syntax() instanceof TopicSyntax topic) - || topic.baseTopic().isEmpty()) return false; - current = resolver.resolve(current, topic.baseTopic().orElseThrow(), Set.of(SymbolKind.TOPIC)) - .target().orElse(null); - } - return false; - } - - private SymbolId contextualObject(SymbolId target, SymbolId topic) { - if (target == null) return null; - Set seen = new HashSet<>(); - for (SymbolId context = topic; context != null && seen.add(context); context = baseOf.get(context)) { - for (DeclInfo candidate : index.children(context)) { - checkWork(); - if ((candidate.kind() == SymbolKind.CLASS || candidate.kind() == SymbolKind.ASSOCIATION) - && candidate.modifiers().contains(guru.interlis.model.Modifier.EXTENDED) - && guru.interlis.model.internal.TypeCompatibility.specializes(candidate.id(), target, baseOf::get)) { - return candidate.id(); - } - } - } - return target; - } - - private SymbolId commonRoleScope(RoleSymbol role, SymbolId topic) { - List roots = role.targetRoots().stream().map(t -> contextualObject(t, topic)).toList(); - return commonObjectScope(roots); - } - - SymbolId commonObjectScope(List roots) { - if (roots.isEmpty()) return null; - // Only a common hierarchy admits a subsequent statically bound member. - for (SymbolId candidate : baseChain(roots.getFirst())) { - if (roots.stream().allMatch(t -> targetSpecializes(t, candidate))) return candidate; - } - return null; - } - - private guru.interlis.model.ObjectArgumentType rolePathType(TypeId id, RoleSymbol role, - SymbolId topic, SourceRange range, boolean many) { - List roots = role.targetRoots().stream().map(t -> contextualObject(t, topic)).toList(); - boolean any = roots.stream().anyMatch(t -> objectBase(t).isEmpty()); - return new guru.interlis.model.ObjectArgumentType(id, new Origin.DeclaredOrigin(range), - !any && roots.size() == 1 ? Optional.of(roots.getFirst()) : Optional.empty(), - any || roots.size() == 1 ? List.of() : roots, many); - } - - /** Same participant-value type as an ordinary role path, for implicit 2.4 - * ALL OF values. One basis row denotes one link, not all its peer links. */ - TypeId roleValueType(RoleSymbol role,SymbolId owner,SourceRange range) { - TypeId id=nextTypeId();types.put(id,rolePathType(id,role,scopeRoot(owner),range,false));return id; - } - - TypeId objectPathType(SymbolId target, SourceRange range) { - TypeId type = nextTypeId(); - if (objectBase(target).isEmpty()) { - types.put(type, new guru.interlis.model.ObjectArgumentType(type, new Origin.DeclaredOrigin(range), - Optional.empty(), List.of(), false)); - } else if (index.byId(target).map(DeclInfo::kind).orElse(null) == SymbolKind.STRUCTURE) { - types.put(type, new StructureValueType(type, new Origin.DeclaredOrigin(range), Optional.of(target), List.of())); - } else { - types.put(type, new ReferenceType(type, new Origin.DeclaredOrigin(range), Optional.of(target), List.of(), false)); - } - return type; - } - - Optional findMember(SymbolId owner, String name) { - SymbolId current = owner; - Set seen = new HashSet<>(); - while (current != null && seen.add(current)) { - checkWork(); - for (DeclInfo child : index.children(current)) { - checkWork(); - if ((child.kind() == SymbolKind.ATTRIBUTE || child.kind() == SymbolKind.ROLE || child.kind() == SymbolKind.VIEW_BASE) - && child.name().equals(name)) { - return Optional.of(child.id()); - } - } - Symbol predefinedOwner = symbols.get(current); - if (predefinedOwner != null && predefinedOwner.origin() instanceof Origin.PredefinedOrigin) { - var members = predefinedOwner instanceof StructureSymbol s ? s.members() : predefinedOwner instanceof ClassSymbol c ? c.members() : List.of(); - for (SymbolId member : members) if (symbols.get(member).kind() == SymbolKind.ATTRIBUTE && symbols.get(member).name().value().equals(name)) return Optional.of(member); - } - var viewMember = views.member(current, name); - if (viewMember.isPresent()) return viewMember; - current = objectInheritanceBase(current).orElse(null); - } - return Optional.empty(); - } - - private ExpressionResult binaryExpression(ExpressionId id, BinaryExpressionSyntax syntax, - SymbolId owner) { - ExpressionResult left = buildExpression(syntax.left(), owner); - ExpressionResult right = buildExpression(syntax.right(), owner); - TypeId result = builtinBoolean(index.byId(owner).orElseThrow().language()); - boolean error = left.error() || right.error(); - switch (syntax.operator()) { - case AND, OR, IMPLIES -> { - if (!error && (!isBoolean(left.typeId()) || !isBoolean(right.typeId()))) { - emitTypeMismatch(ownerSource(owner), syntax.range(), - typeName(left.typeId()) + "," + typeName(right.typeId()), "BOOLEAN"); - error = true; - } - } - case EQ, NEQ, LT, LTE, GT, GTE -> { - var comparison=syntax.operator()==guru.interlis.model.BinaryOperator.EQ || syntax.operator()==guru.interlis.model.BinaryOperator.NEQ - ?guru.interlis.model.ComparisonOperator.EQUALITY:guru.interlis.model.ComparisonOperator.ORDERING; - left=contextualNumericConstant(left,right.typeId()); - right=contextualNumericConstant(right,left.typeId()); - left=contextualValue(left,right.typeId(),owner); - right=contextualValue(right,left.typeId(),owner); - error |= left.error() || right.error(); - if(!error && !comparisonAllowed(left,right,comparison)) { - emitTypeMismatch(ownerSource(owner),syntax.range(),typeName(left.typeId())+","+typeName(right.typeId()),"COMPARABLE "+comparison); - error=true; - } - } - case PLUS, MINUS, MUL, DIV, POW -> { - result = left.typeId(); - if (!error && (!isNumeric(left.typeId()) || !isNumeric(right.typeId()))) { - emitTypeMismatch(ownerSource(owner), syntax.range(), - typeName(left.typeId()) + "," + typeName(right.typeId()), "NUMERIC"); - error = true; - } - } - } - TypedExpression expression = new guru.interlis.model.BinaryExpression(id, result, - error ? guru.interlis.model.ExpressionTypeState.ERROR - : guru.interlis.model.ExpressionTypeState.RESOLVED, - syntax.range(), List.of(left.id(), right.id()), syntax.operator(), - left.id(), right.id()); - expressions.put(id, expression); - return new ExpressionResult(id, result, error); - } - - private ExpressionResult unaryExpression(ExpressionId id, UnaryExpressionSyntax syntax, - SymbolId owner) { - ExpressionResult operand = buildExpression(syntax.operand(), owner); - TypeId type = builtinBoolean(index.byId(owner).orElseThrow().language()); - boolean error = operand.error(); - if (!error && syntax.operator() == guru.interlis.model.UnaryOperator.NOT && !isBoolean(operand.typeId())) { - emitTypeMismatch(ownerSource(owner), syntax.range(), typeName(operand.typeId()), - "BOOLEAN"); - error = true; - } - TypedExpression expression = new guru.interlis.model.UnaryExpression(id, type, - error ? guru.interlis.model.ExpressionTypeState.ERROR - : guru.interlis.model.ExpressionTypeState.RESOLVED, - syntax.range(), List.of(operand.id()), syntax.operator(), operand.id()); - expressions.put(id, expression); - return new ExpressionResult(id, type, error); - } - - private ExpressionResult definedExpression(ExpressionId id, DefinedExpressionSyntax syntax, - SymbolId owner) { - ExpressionResult operand = buildExpression(syntax.operand(), owner); - TypeId type = builtinBoolean(index.byId(owner).orElseThrow().language()); - TypedExpression expression = new guru.interlis.model.DefinedExpression(id, type, - operand.error() ? guru.interlis.model.ExpressionTypeState.ERROR - : guru.interlis.model.ExpressionTypeState.RESOLVED, - syntax.range(), List.of(operand.id()), operand.id()); - expressions.put(id, expression); - return new ExpressionResult(id, type, operand.error()); - } - - private ExpressionResult constantExpression(ExpressionId id, ConstantExpressionSyntax syntax, SymbolId owner) { - TypeId type = numericLiteralType(Optional.empty(), syntax.unit(), syntax.range(), owner); - TypedExpression expression = new guru.interlis.model.ConstantExpression(id, type, - effectiveDescriptor(type) instanceof ErrorType ? guru.interlis.model.ExpressionTypeState.ERROR : guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of(), - syntax.constantName()); - expressions.put(id, expression); - return new ExpressionResult(id, type, effectiveDescriptor(type) instanceof ErrorType); - } - - private ExpressionResult undefinedExpression(ExpressionId id, UndefinedExpressionSyntax syntax, SymbolId owner) { - TypeId type = nextTypeId(); - types.put(type, new guru.interlis.model.GenericType(type, - new Origin.PredefinedOrigin(index.byId(owner).orElseThrow().language(), "UNDEFINED"), - "UNDEFINED")); - TypedExpression expression = new guru.interlis.model.UndefinedExpression(id, type, - guru.interlis.model.ExpressionTypeState.RESOLVED, syntax.range(), List.of()); - expressions.put(id, expression); - return new ExpressionResult(id, type, false); - } - - /** A missing parsed condition is a source recovery error, never an implementation gap. */ - private ExpressionResult missingConstraintExpression(DeclInfo owner,SourceRange range) { - var causes=syntaxErrors.stream().filter(d->d.primaryRange().filter(r->r.source().equals(range.source()) - && r.revision().equals(range.revision()) && r.startOffset()<=range.endOffset() && r.endOffset()>=range.startOffset()).isPresent()) - .map(guru.interlis.diagnostics.Diagnostic::id).toList(); - if(causes.isEmpty())causes=List.of(emitRule(owner,"constraint syntax is incomplete","SOURCE_MISSING_CONSTRAINT",range)); - return failedValueExpression(new ExpressionId("expr-"+(++expressionCounter)),range,causes); - } - - private ExpressionResult errorExpression(SourceId source, SourceRange range, String feature) { - ExpressionId id = new ExpressionId("expr-" + (++expressionCounter)); - TypeId type = errorType(source, range, feature); - DiagnosticId diagnostic = emitUnsupported(source, range, feature, feature); - TypedExpression expression = new ErrorExpression(id, type, - guru.interlis.model.ExpressionTypeState.ERROR, range, List.of(), - List.of(diagnostic)); - expressions.put(id, expression); - return new ExpressionResult(id, type, true); - } - - private boolean formattedTextValid(ExpressionResult left, ExpressionResult right) { - TypeDescriptor leftDescriptor = effectiveDescriptor(left.typeId()); - TypeDescriptor rightDescriptor = effectiveDescriptor(right.typeId()); - guru.interlis.model.FormattedType formatted; - ExpressionId textId; - if (leftDescriptor instanceof guru.interlis.model.FormattedType f) { - formatted = f; - textId = right.id(); - } else if (rightDescriptor instanceof guru.interlis.model.FormattedType f) { - formatted = f; - textId = left.id(); - } else { - return true; - } - TypedExpression expression = expressions.get(textId); - if (!(expression instanceof guru.interlis.model.LiteralExpression literal) - || literal.textValue().isEmpty()) { - return true; - } - String value = literal.textValue().orElseThrow(); - return formattedValues().parse(formatted, value, true).isPresent(); - } - - private boolean isEnumTreeType(TypeId id) { - return effectiveDescriptor(id) instanceof EnumerationType enumeration && enumeration.treeValues(); - } - - private boolean isEnumConstant(ExpressionResult result) { - TypedExpression expression = expressions.get(result.id()); - return expression instanceof guru.interlis.model.ConstantExpression constant - && constant.constantName().startsWith("#"); - } - - private boolean enumConstantValid(ExpressionResult left, ExpressionResult right) { - ExpressionResult enumSide = null; - ExpressionResult constSide = null; - if (effectiveDescriptor(left.typeId()) instanceof EnumerationType) { - enumSide = left; - constSide = right; - } else if (effectiveDescriptor(right.typeId()) instanceof EnumerationType) { - enumSide = right; - constSide = left; - } - if (enumSide == null || constSide == null) { - return true; - } - TypedExpression constant = expressions.get(constSide.id()); - if (!(constant instanceof guru.interlis.model.ConstantExpression value) - || !value.constantName().startsWith("#")) { - return true; - } - String path = value.constantName().substring(1); - EnumerationType enumeration = (EnumerationType) effectiveDescriptor(enumSide.typeId()); - boolean exact = enumeration.values().stream().anyMatch(v -> v.path().equals(path)); - boolean hasChildren = enumeration.values().stream().anyMatch(v -> v.path().startsWith(path + ".")); - return exact && (!hasChildren || isEnumTreeType(enumSide.typeId())); - } - - /** Bind constants to their expected value type; shared inherited expressions are only checked by valueAssignable. */ - ExpressionResult contextualValue(ExpressionResult value, TypeId expected, SymbolId owner) { - if(value.error())return value; - value=contextualNumericConstant(value,expected); - if (expressions.get(value.id()) instanceof guru.interlis.model.FunctionCallExpression call && isUnitConversion(call) - && effectiveDescriptor(expected) instanceof NumericType numeric) { - // Unrestricted NUMERIC is a wildcard formal, not a new unitless destination. - if(numeric.range().unit().isEmpty() && numeric.range().minimum().isEmpty() && numeric.range().maximum().isEmpty())return value; - TypeId type=expected;boolean error=!conversionCompatible(call,numeric); - if(error) { - var diagnostic=emitRule(index.byId(owner).orElseThrow(),"convertUnit source and destination need a common conversion unit","CONVERT_UNIT",call.range()); - type=nextTypeId();types.put(type,new ErrorType(type,new Origin.DeclaredOrigin(call.range()),List.of(diagnostic))); - } - expressions.put(call.id(),new guru.interlis.model.FunctionCallExpression(call.id(),type, - error?guru.interlis.model.ExpressionTypeState.ERROR:call.typeState(),call.range(),call.children(),call.functionName(),call.targetFunction(),call.argumentPaths())); - return new ExpressionResult(call.id(),type,error); - } - var result=contextualEnumConstant(value,expected,owner); - if(expressions.get(result.id()) instanceof guru.interlis.model.LiteralExpression literal && literal.textValue().isPresent() - && effectiveDescriptor(expected) instanceof FormattedType formatted - && formattedValues().parse(formatted,literal.textValue().orElseThrow(),true).isPresent()) { - expressions.put(result.id(),new guru.interlis.model.LiteralExpression(literal.id(),expected,literal.typeState(),literal.range(),literal.children(),literal.literalKind(),literal.lexeme(),literal.decimal(),literal.textValue())); - return new ExpressionResult(result.id(),expected,false); - } - return result; - } - - private ExpressionResult contextualNumericConstant(ExpressionResult value, TypeId expected) { - if(value.error() || !(effectiveDescriptor(expected) instanceof NumericType target) || target.range().unit().isEmpty() - || !(effectiveDescriptor(value.typeId()) instanceof NumericType source) || source.range().unit().isPresent())return value; - var expression=expressions.get(value.id()); - if(!(expression instanceof guru.interlis.model.LiteralExpression literal && literal.literalKind()==guru.interlis.model.LiteralKind.NUMBER) - && !(expression instanceof guru.interlis.model.ConstantExpression constant && (constant.constantName().equals("PI") || constant.constantName().equals("LNBASE"))))return value; - TypeId type=nextTypeId();var range=source.range(); - types.put(type,new NumericType(type,source.origin(),new NumericRange(range.minimum(),range.maximum(),range.circular(),range.direction(),target.range().unit(),range.referenceSystem()))); - if(expression instanceof guru.interlis.model.LiteralExpression literal) - expressions.put(value.id(),new guru.interlis.model.LiteralExpression(literal.id(),type,literal.typeState(),literal.range(),literal.children(),literal.literalKind(),literal.lexeme(),literal.decimal(),literal.textValue())); - else {var constant=(guru.interlis.model.ConstantExpression)expression; - expressions.put(value.id(),new guru.interlis.model.ConstantExpression(constant.id(),type,constant.typeState(),constant.range(),constant.children(),constant.constantName()));} - return new ExpressionResult(value.id(),type,false); - } - - private boolean isUnitConversion(guru.interlis.model.FunctionCallExpression call) { - return call.targetFunction().flatMap(id->Optional.ofNullable(symbols.get(id))).filter(FunctionSymbol.class::isInstance) - .map(Symbol::origin).filter(Origin.PredefinedOrigin.class::isInstance).map(Origin.PredefinedOrigin.class::cast) - .map(origin->origin.key().equals("INTERLIS.convertUnit")).orElse(false); - } - - private boolean conversionCompatible(guru.interlis.model.FunctionCallExpression call,NumericType target) { - var descriptor=effectiveDescriptor(expressions.get(call.children().getFirst()).typeId()); - if(guru.interlis.model.internal.TypeCompatibility.isUndefined(descriptor))return true; - var from=(NumericType)descriptor; - var sourceUnit=from.range().unit();var targetUnit=target.range().unit(); - if(sourceUnit.isEmpty() || targetUnit.isEmpty())return sourceUnit.isEmpty() && targetUnit.isEmpty(); - return guru.interlis.model.internal.TypeCompatibility.unitRelation(sourceUnit.orElseThrow(),targetUnit.orElseThrow(),this::extensionUnit) - .map(relation->relation.decision()==guru.interlis.model.RelationDecision.ALLOWED).orElse(false); - } - - boolean valueAssignable(TypedExpression value, TypeId expected) { - var descriptor=effectiveDescriptor(expected); - if(value instanceof guru.interlis.model.FunctionCallExpression call && isUnitConversion(call) && descriptor instanceof NumericType numeric) - return conversionCompatible(call,numeric); - if(value instanceof guru.interlis.model.ConstantExpression constant && constant.constantName().startsWith("#")) { - var paths=enumerationPaths(descriptor);String path=constant.constantName().substring(1); - if(!paths.isEmpty())return paths.contains(path) && (!(descriptor instanceof EnumerationType enumeration) || enumeration.treeValues() - || paths.stream().noneMatch(p->p.startsWith(path+"."))); - } - if(value instanceof guru.interlis.model.LiteralExpression literal && literal.textValue().isPresent() && descriptor instanceof FormattedType formatted) - return formattedValues().parse(formatted,literal.textValue().orElseThrow(),true).isPresent(); - return assignable(value.typeId(),expected); - } - - private ExpressionResult contextualEnumConstant(ExpressionResult value, TypeId context, SymbolId owner) { - if(!isEnumConstant(value))return value; - var actual=(guru.interlis.model.ConstantExpression)expressions.get(value.id()); - TypeDescriptor expected=effectiveDescriptor(context); - String path=actual.constantName().substring(1); - var paths=enumerationPaths(expected); - boolean valid=paths.contains(path) && (!(expected instanceof EnumerationType e) || e.treeValues() - || paths.stream().noneMatch(p->p.startsWith(path+"."))); - if(!valid)return value; - expressions.put(value.id(),new guru.interlis.model.ConstantExpression(actual.id(),context,actual.typeState(),actual.range(),actual.children(),actual.constantName())); - if(expected instanceof EnumerationType enumeration) { - var segments=new ArrayList();int start=actual.range().startOffset()+1;String prefix=""; - for(String part:path.split("\\.")) { - prefix=prefix.isEmpty()?part:prefix+"."+part; - var target=enumMember(enumeration,prefix); - var range=new SourceRange(actual.range().source(),actual.range().revision(),start,start+part.length()); - segments.add(new ReferenceSegment(part,range,Optional.ofNullable(target)));start+=part.length()+1; - } - SymbolId target=segments.getLast().target().orElse(null); - var range=new SourceRange(actual.range().source(),actual.range().revision(),actual.range().startOffset()+1,actual.range().endOffset()); - if(target!=null && recordedReferences.add(new ReferenceSite(owner,range,ReferenceUsage.EXPRESSION,Optional.of(target)))) { - var id=new ReferenceId("ref-"+(++referenceCounter)); - references.put(id,new NameReference(id,new ScopeId(owner.value()),ReferenceUsage.EXPRESSION,segments,range,new BindingResolution.BoundTarget(target))); - } - } - return new ExpressionResult(value.id(),context,value.error()); - } - - private SymbolId enumMember(EnumerationType type,String path) { - Set seen=new HashSet<>(); - for(var current=type;current!=null && seen.add(current.id());) { - for(var entry:declaredTypes.entrySet()) { - checkWork();TypeDescriptor candidate=effectiveDescriptor(entry.getValue()); - while(candidate instanceof CollectionType collection)candidate=effectiveDescriptor(collection.elementType()); - if(candidate==null || !candidate.id().equals(current.id()))continue; - for(var child:index.children(entry.getKey()))if(child.kind()==SymbolKind.ENUM_ELEMENT && child.name().equals(path))return child.id(); - } - var base=current.baseType().map(this::effectiveDescriptor).orElse(null);current=base instanceof EnumerationType e?e:null; - } - return null; - } - - private boolean comparisonAllowed(ExpressionResult left, ExpressionResult right, guru.interlis.model.ComparisonOperator operator) { - if(!enumConstantValid(left,right) || !formattedTextValid(left,right))return false; - TypeDescriptor a=effectiveDescriptor(left.typeId()),b=effectiveDescriptor(right.typeId()); - if(isEnumConstant(left) && b instanceof EnumerationType)a=b; - if(isEnumConstant(right) && a instanceof EnumerationType)b=a; - if(a instanceof FormattedType && expressions.get(right.id()) instanceof guru.interlis.model.LiteralExpression literal - && literal.literalKind()==guru.interlis.model.LiteralKind.TEXT)b=a; - if(b instanceof FormattedType && expressions.get(left.id()) instanceof guru.interlis.model.LiteralExpression literal - && literal.literalKind()==guru.interlis.model.LiteralKind.TEXT)a=b; - return guru.interlis.model.internal.TypeCompatibility.compare(a,b,operator,this::effectiveDescriptor,id->effectiveDescriptor(declaredTypes.get(id))).decision() - ==guru.interlis.model.RelationDecision.ALLOWED; - } - - boolean isBoolean(TypeId typeId) { - var type=effectiveDescriptor(typeId); - return type instanceof guru.interlis.model.BooleanType || guru.interlis.model.internal.TypeCompatibility.isUndefined(type); - } - - private boolean isNumeric(TypeId typeId) { - var type=effectiveDescriptor(typeId); - return type instanceof NumericType || guru.interlis.model.internal.TypeCompatibility.isUndefined(type); - } - - TypeDescriptor effectiveDescriptor(TypeId typeId) { - TypeDescriptor descriptor = types.get(typeId); - Set seen = new HashSet<>(); - while (descriptor instanceof DomainReferenceType reference && seen.add(descriptor.id())) { - checkWork(); - TypeId target = declaredTypes.get(reference.targetDomain()); - if (target == null) break; - descriptor = types.get(target); - } - return descriptor; - } - - String typeName(TypeId id) { - TypeDescriptor descriptor = effectiveDescriptor(id); - if(descriptor instanceof guru.interlis.model.EnumArgumentType argument)return argument.treeValues()?"ENUMTREEVAL":"ENUMVAL"; - if (descriptor instanceof TextType text) { - return text.textKind().name() + (text.maxLength().isPresent() ? "*" + text.maxLength().getAsInt() : ""); - } - return descriptor == null ? "UNKNOWN" : descriptor.kind().name(); - } - - DiagnosticId emitTypeMismatch(SourceId source, SourceRange range, String actual, String expected) { - return collector.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0005"), Severity.ERROR, - Phase.TYPE_CHECKING, "diagnostic.semantics.type") - .at(range) - .argument(new DiagnosticArgument.TypeNameArgument("actual", actual)) - .argument(new DiagnosticArgument.TypeNameArgument("expected", expected))); - } - - private Optional decimal(String lexeme) { - try { - return Optional.of(DecimalLiteral.parse(lexeme)); - } catch (RuntimeException e) { - return Optional.empty(); - } - } - - private SourceId ownerSource(SymbolId owner) { - return index.modelOfSymbol(owner).flatMap(index::byId) - .map(DeclInfo::sourceId).orElseGet(() -> sourceSet.sources().keySet().iterator().next()); - } - - // ---- overrides, effective members and types -------------------------------- - - private final Set effectiveProcessed = new HashSet<>(); - - private void resolveOverridesAndEffectiveMembers() { - for (IndexedDeclarations.ModelEntry entry : index.models()) { - checkWork(); - for (DeclInfo child : index.children(entry.id())) { - checkWork(); - collectEffective(child); - } - } - } - - void collectEffective(DeclInfo decl) { - if (!effectiveProcessed.add(decl.id())) { - return; - } - if (decl.kind() == SymbolKind.CLASS || decl.kind() == SymbolKind.STRUCTURE - || decl.kind() == SymbolKind.ASSOCIATION) { - if (decl.kind() != SymbolKind.ASSOCIATION) metadata.ensureBases(decl.id()); - List chain = baseChain(decl.id()); - if (hasCycle(chain)) { - emitCycle(decl); - effectiveMembers.put(decl.id(), localMembers(decl)); - return; - } - List result = new ArrayList<>(); - Map positions = new HashMap<>(); - for (int depth = chain.size() - 1; depth >= 0; depth--) { - checkWork(); - SymbolId current = chain.get(depth); - if (symbols.get(current) != null && symbols.get(current).origin() instanceof Origin.PredefinedOrigin) { - var predefined = symbols.get(current); - var members = predefined instanceof StructureSymbol s ? s.members() : predefined instanceof ClassSymbol c ? c.members() : List.of(); - for (SymbolId member : members) { - if (symbols.get(member).kind() != SymbolKind.ATTRIBUTE) continue; - String name = symbols.get(member).name().value(); - Integer existing = positions.get(name); - int position = existing == null ? result.size() : existing; - var effective = new EffectiveMember(member, decl.id(), pathTo(chain, depth), position, - EffectiveMemberOrigin.INHERITED, existing != null); - if (existing == null) { positions.put(name, position); result.add(effective); } - else result.set(position, effective); - } - } - for (DeclInfo attribute : index.children(current)) { - checkWork(); - if (attribute.kind() != SymbolKind.ATTRIBUTE && attribute.kind() != SymbolKind.ROLE) { - continue; - } - buildSymbol(attribute); - Integer existing = positions.get(attribute.name()); - if (existing != null) { - if (depth == 0 && !attribute.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { - emitRule(attribute, "override requires EXTENDED", "OVERRIDE"); - } - EffectiveMember old = result.get(existing); - if (depth == 0 && attribute.kind() == SymbolKind.ATTRIBUTE && isFinalDeclaration(old.declaredSymbol())) - emitRule(attribute, "cannot override FINAL attribute", "ATTRIBUTE_FINAL"); - result.set(existing, new EffectiveMember(attribute.id(), decl.id(), - pathTo(chain, depth), existing, - depth == 0 ? EffectiveMemberOrigin.OVERRIDDEN : EffectiveMemberOrigin.INHERITED, true)); - AttributeSymbol override = attribute.kind() == SymbolKind.ATTRIBUTE - ? overrideAttribute(attribute.id(), old.declaredSymbol()) : null; - if (override != null) { - symbols.put(attribute.id(), override); - } - } else { - if (depth == 0 && attribute.kind() == SymbolKind.ATTRIBUTE - && attribute.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) - emitRule(attribute, "EXTENDED attribute needs an inherited attribute", "EXTENDED_BASE"); - int position = result.size(); - positions.put(attribute.name(), position); - result.add(new EffectiveMember(attribute.id(), decl.id(), - pathTo(chain, depth), position, - depth == 0 ? EffectiveMemberOrigin.LOCAL - : EffectiveMemberOrigin.INHERITED, false)); - } - } - } - effectiveMembers.put(decl.id(), List.copyOf(result)); - } - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - collectEffective(child); - } - } - - DiagnosticId emitRule(DeclInfo decl, String rule, String reason) { - return emitRule(decl, rule, reason, ruleRange(decl)); - } - - private SourceRange ruleRange(DeclInfo decl) { - return decl.origin() instanceof Origin.DeclaredOrigin declared - ? declared.range() : decl.nameRange().orElse(null); - } - - DiagnosticId emitRule(DeclInfo decl, String rule, String reason, SourceRange range) { - return emitRule(decl, rule, reason, range, List.of()); - } - - private DiagnosticId emitRule(DeclInfo decl, String rule, String reason, SourceRange range, - List related) { - var draft = DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0010"), - Severity.ERROR, Phase.SEMANTIC_CHECKING, - "diagnostic.semantics.inheritance") - .at(range) - .argument(new DiagnosticArgument.IdentifierArgument("element", decl.name())) - .argument(new DiagnosticArgument.TextArgument("rule", rule + "/" + reason)); - related.forEach(draft::related); - return collector.emit(draft); - } - - SymbolId scopeRoot(SymbolId symbol) { - SymbolId current = symbol; - Set seen = new HashSet<>(); - while (current != null && seen.add(current)) { - checkWork(); - DeclInfo info = index.byId(current).orElse(null); - if (info == null) { - return null; - } - if (info.kind() == SymbolKind.TOPIC || info.kind() == SymbolKind.MODEL) { - return current; - } - current = info.owner().orElse(null); - } - return null; - } - - private AttributeSymbol overrideAttribute(SymbolId id, SymbolId baseId) { - if (symbols.get(id) instanceof AttributeSymbol attribute) { - return new AttributeSymbol(attribute.header(), attribute.declaredType(), - attribute.mandatory(), attribute.transientAttribute(), attribute.continuous(), - attribute.subdivision(), Optional.of(baseId), attribute.derivations()); - } - return null; - } - - List baseChain(SymbolId id) { - List chain = new ArrayList<>(); - SymbolId current = id; - Set seen = new HashSet<>(); - while (current != null) { - checkWork(); - chain.add(current); - if (!seen.add(current)) break; - current = baseOf.get(current); - } - return chain; - } - - private boolean hasCycle(List chain) { - return chain.size() != new HashSet<>(chain).size(); - } - - private void emitCycle(DeclInfo decl) { - collector.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0006"), Severity.ERROR, - Phase.SEMANTIC_CHECKING, "diagnostic.semantics.cycle") - .at(decl.origin() instanceof Origin.DeclaredOrigin d ? d.range() : decl.nameRange().orElse(null)) - .argument(new DiagnosticArgument.TextArgument("relationship", "INHERITANCE")) - .argument(new DiagnosticArgument.IdentifierListArgument("cycle", - baseChain(decl.id()).stream().map(SymbolId::value).toList()))); - } - - private List localMembers(DeclInfo decl) { - List result = new ArrayList<>(); - int position = 0; - for (DeclInfo child : index.children(decl.id())) { - checkWork(); - if (child.kind() == SymbolKind.ATTRIBUTE) { - result.add(new EffectiveMember(child.id(), decl.id(), List.of(decl.id()), position++, - EffectiveMemberOrigin.LOCAL, false)); - } - } - return List.copyOf(result); - } - - private List pathTo(List chain, int depth) { - List result = new ArrayList<>(); - for (int i = depth; i >= 0; i--) { - checkWork(); - result.add(chain.get(i)); - } - return List.copyOf(result); - } - - private void buildEffectiveTypes() { - for (Map.Entry entry : declaredTypes.entrySet()) { - checkWork(); - effectiveTypes.put(entry.getKey(), effectiveType(entry.getValue())); - } - } - - private EffectiveType effectiveType(TypeId declared) { - List aliasPath = new ArrayList<>(); - TypeId current = declared; - Set seen = new HashSet<>(); - while (true) { - checkWork(); - TypeDescriptor descriptor = types.get(current); - if (!(descriptor instanceof DomainReferenceType reference)) { - break; - } - if (!seen.add(current)) { - break; - } - aliasPath.add(reference.targetDomain()); - Symbol symbol = symbols.get(reference.targetDomain()); - if (symbol instanceof DomainSymbol domain) { - current = domain.typeId(); - } else { - break; - } - } - return new EffectiveType(declared, current, aliasPath); - } - - // ---- references and coverage ---------------------------------------------- - - private void buildReferences() { - // Walk the complete declaration tree: attributes and roles also carry - // navigable type/role references, not only model-level declarations. - for (IndexedDeclarations.ModelEntry entry : index.models()) { - checkWork(); - if (symbols.get(entry.id()) instanceof ModelSymbol model) { - for (var imported : model.imports()) { - var name = new guru.interlis.syntax.NameSyntax( - new guru.interlis.syntax.SyntaxNodeId("import-" + referenceCounter), - imported.range(), List.of(new guru.interlis.syntax.NameSyntax.NameSegmentSyntax( - imported.importedModel(), imported.range()))); - recordReference(entry.declaration(), name, imported.targetModel().orElse(null), ReferenceUsage.IMPORT); - } - } - referenceTree(entry.id()); - } - } - - private void referenceTree(SymbolId owner) { - for (DeclInfo child : index.children(owner)) { - checkWork(); - referenceFor(child); - referenceTree(child.id()); - } - } - - private void referenceFor(DeclInfo decl) { - if (decl.syntax() instanceof ClassDeclarationSyntax clazz && clazz.baseType().isPresent()) { - recordReference(decl, clazz.baseType().get(), baseOf.get(decl.id()), ReferenceUsage.BASE_TYPE); - } else if (decl.syntax() instanceof StructureDeclarationSyntax structure - && structure.baseType().isPresent()) { - recordReference(decl, structure.baseType().get(), baseOf.get(decl.id()), - ReferenceUsage.BASE_TYPE); - } else if (decl.syntax() instanceof TopicSyntax topic && topic.baseTopic().isPresent()) { - recordReference(decl, topic.baseTopic().get(), baseOf.get(decl.id()), - ReferenceUsage.BASE_TYPE); - } else if (decl.syntax() instanceof DomainDeclarationSyntax domain - && domain.baseDomain().isPresent()) { - recordReference(decl, domain.baseDomain().get(), baseOf.get(decl.id()), - ReferenceUsage.BASE_TYPE); - } else if (decl.syntax() instanceof AssociationDeclarationSyntax association - && association.baseAssociation().isPresent()) { - recordReference(decl, association.baseAssociation().orElseThrow(), baseOf.get(decl.id()), ReferenceUsage.BASE_TYPE); - } else if (decl.syntax() instanceof AttributeDeclarationSyntax attribute) { - referenceForType(decl, attribute.type()); - } - } - - private void referenceForType(DeclInfo decl, TypeSyntax syntax) { - if (syntax instanceof DomainReferenceSyntax reference) { - TypeDescriptor descriptor = types.get(declaredTypes.get(decl.id())); - if (descriptor instanceof DomainReferenceType domain) { - recordReference(decl, reference.name(), domain.targetDomain(), - ReferenceUsage.TYPE_USE); - } - } else if (syntax instanceof CollectionTypeSyntax collection) { - referenceForType(decl, collection.elementType()); - } - } - - private record ReferenceSite(SymbolId owner, SourceRange range, ReferenceUsage usage, Optional target) { } - - void recordReference(DeclInfo owner, guru.interlis.syntax.NameSyntax name, - SymbolId target, ReferenceUsage usage) { - if (!recordedReferences.add(new ReferenceSite(owner.id(), name.range(), usage, Optional.ofNullable(target)))) return; - ReferenceId id = new ReferenceId("ref-" + (++referenceCounter)); - List segments = new ArrayList<>(); - SymbolId current = target; - for (int i = name.segments().size() - 1; i >= 0; i--) { - checkWork(); - var segment = name.segments().get(i); - if (i < name.segments().size() - 1) { - current = referenceOwner(current); - while (current != null && !referenceName(current).equals(segment.text())) { - checkWork(); - current = referenceOwner(current); - } - } - segments.addFirst(new ReferenceSegment(segment.text(), segment.range(), Optional.ofNullable(current))); - } - BindingResolution resolution = target == null - ? new BindingResolution.UnresolvedTarget(guru.interlis.model.UnresolvedReason.NAME_NOT_FOUND, - List.of()) - : new BindingResolution.BoundTarget(target); - references.put(id, new NameReference(id, new ScopeId(owner.id().value()), usage, - segments, name.range(), resolution)); - } - - private SymbolId referenceOwner(SymbolId symbol) { - if (symbol == null) return null; - var declaration = index.byId(symbol); - if (declaration.isPresent()) return declaration.orElseThrow().owner().orElse(null); - var value = symbols.get(symbol); - if (value == null) return null; - if (value.owner().isPresent()) return value.owner().orElseThrow(); - return value.origin() instanceof Origin.PredefinedOrigin predefined && predefined.key().startsWith("INTERLIS.") - ? PredefinedModels.ensure(this, predefined.language()) : null; - } - - private String referenceName(SymbolId symbol) { - return index.byId(symbol).map(DeclInfo::name).orElseGet(() -> { - String name = symbols.get(symbol).name().value(); return name.substring(name.lastIndexOf('.') + 1); - }); - } - - private AnalysisCoverage buildCoverage() { - List entries = new ArrayList<>(); - List diagnostics = collector.snapshot().stream() - .filter(d -> d.phase() == Phase.LEXING || d.phase() == Phase.PARSING - || d.primaryRange().map(closure::contains).orElse(true)).toList(); - for (SourceId source : closure.sources()) { - checkWork(); - boolean lexing = diagnostics.stream().noneMatch(d -> d.primaryRange() - .filter(r -> r.source().equals(source)).isPresent() - && d.code().value().equals("ILI-SYN-0001")); - boolean parsing = diagnostics.stream().noneMatch(d -> d.primaryRange() - .filter(r -> r.source().equals(source)).isPresent() - && d.code().value().matches("ILI-SYN-000[2-6]")); - boolean indexing = diagnostics.stream().noneMatch(d -> d.code().value().equals("ILI-SEM-0003") - && d.primaryRange().filter(r -> r.source().equals(source)).isPresent()); - boolean binding = diagnostics.stream().noneMatch(d -> d.code().value().matches("ILI-SEM-000[124]") - && d.primaryRange().filter(r -> r.source().equals(source)).isPresent()); - boolean typeChecking = diagnostics.stream().noneMatch(d -> d.code().value().matches("ILI-SEM-000[578]") - && d.primaryRange().filter(r -> r.source().equals(source)).isPresent()); - boolean semantic = diagnostics.stream().noneMatch(d -> d.code().value().matches("ILI-SEM-0006|ILI-SEM-0010") - && d.primaryRange().filter(r -> r.source().equals(source)).isPresent()); - entries.add(entry(Phase.LEXING, source, lexing ? CoverageStatus.COMPLETE : CoverageStatus.BLOCKED_BY_ERROR)); - entries.add(entry(Phase.PARSING, source, parsing ? CoverageStatus.COMPLETE : CoverageStatus.BLOCKED_BY_ERROR)); - entries.add(entry(Phase.DECLARATION_INDEXING, source, - indexing ? CoverageStatus.COMPLETE : CoverageStatus.BLOCKED_BY_ERROR)); - entries.add(entry(Phase.BINDING, source, - binding ? CoverageStatus.COMPLETE : CoverageStatus.BLOCKED_BY_ERROR)); - entries.add(entry(Phase.TYPE_CHECKING, source, - typeChecking ? CoverageStatus.COMPLETE : CoverageStatus.BLOCKED_BY_ERROR)); - entries.add(entry(Phase.SEMANTIC_CHECKING, source, - semantic ? CoverageStatus.COMPLETE : CoverageStatus.BLOCKED_BY_ERROR)); - } - for (var diagnostic : diagnostics) { - if (diagnostic.code().value().equals("ILI-IMPL-0001")) { - var feature = diagnostic.arguments().stream() - .filter(a -> a instanceof DiagnosticArgument.TextArgument) - .map(a -> ((DiagnosticArgument.TextArgument) a).value()).findFirst(); - entries.add(new AnalysisCoverage.CoverageEntry(Phase.SEMANTIC_CHECKING, - diagnostic.primaryRange().map(SourceRange::source), CoverageStatus.NOT_IMPLEMENTED, - feature, Optional.of(diagnostic.id()))); - } - } - return new AnalysisCoverage(entries); - } - - private AnalysisCoverage.CoverageEntry entry(Phase phase, SourceId source, - CoverageStatus status) { - return new AnalysisCoverage.CoverageEntry(phase, Optional.of(source), status, - Optional.empty(), Optional.empty()); - } - - private Optional findModelId(String name, int offset, SourceId source) { - return index.models().stream() - .filter(e -> e.model().sourceId().equals(source)) - .filter(e -> e.model().name().equals(name)) - .filter(e -> e.model().declarationOffset() == offset) - .map(IndexedDeclarations.ModelEntry::id) - .findFirst(); - } - - private static String unquote(String text) { - return InterlisStrings.decode(text).text(); - } - - record GraphResult(SemanticGraph graph, AnalysisCoverage coverage, List roots, - List models, Map> unsupportedBySource) { - GraphResult { - Objects.requireNonNull(graph, "graph"); - Objects.requireNonNull(coverage, "coverage"); - roots = List.copyOf(Objects.requireNonNull(roots, "roots")); - models = List.copyOf(Objects.requireNonNull(models, "models")); - unsupportedBySource = Map.copyOf(Objects.requireNonNull(unsupportedBySource, - "unsupportedBySource")); - } - } - - record ExpressionResult(ExpressionId id, TypeId typeId, boolean error) { - } - } diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GraphicBuilder.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GraphicBuilder.java index a6eef93e..73aeb854 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GraphicBuilder.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/GraphicBuilder.java @@ -2,113 +2,158 @@ import guru.interlis.model.*; import guru.interlis.syntax.*; + import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; /** Graphics within one compiler run, using its shared name, expression and type binders. */ final class GraphicBuilder { - private final GraphBuilder g; + private final SemanticGraphRead.Symbols state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameBindings names; + private final MetadataBindings metadata; + private final InheritanceBindings inheritance; + private final Supplier expressions; + private final TypeBindings typeBindings; + private final ReferenceBindings references; + private final TopicBindings topics; + private final Function symbolHeader; + private final Function ensureSymbol; + private final Function scopeRoot; private final Set building = new HashSet<>(); private final Map contexts = new HashMap<>(); - GraphicBuilder(GraphBuilder graph) { g=graph; } + + GraphicBuilder(SemanticGraphRead.Symbols state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameBindings names, + MetadataBindings metadata, + InheritanceBindings inheritance, + Supplier expressions, + TypeBindings typeBindings, + ReferenceBindings references, + TopicBindings topics, + Function symbolHeader, + Function ensureSymbol, + Function scopeRoot) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.names = names; + this.metadata = metadata; + this.inheritance = inheritance; + this.expressions = expressions; + this.typeBindings = typeBindings; + this.references = references; + this.topics = topics; + this.symbolHeader = symbolHeader; + this.ensureSymbol = ensureSymbol; + this.scopeRoot = scopeRoot; + } SymbolId context(SymbolId owner) { return contexts.getOrDefault(owner,owner); } GraphicSymbol build(DeclInfo decl) { var syntax=(GraphicDeclarationSyntax)decl.syntax(); if (!building.add(decl.id())) { - g.emitRule(decl,"cyclic graphic inheritance","GRAPHIC_CYCLE"); - return new GraphicSymbol(g.header(decl),Optional.empty(),Optional.empty(),List.of(),List.of(),List.of()); + diagnostics.emitRule(decl,"cyclic graphic inheritance","GRAPHIC_CYCLE"); + return new GraphicSymbol(symbolHeader.apply(decl),Optional.empty(),Optional.empty(),List.of(),List.of(),List.of()); } - var base=syntax.baseGraphic().flatMap(n->g.bindTarget(decl,n,Set.of(SymbolKind.GRAPHIC),ReferenceUsage.BASE_TYPE,new ArrayList<>(),false)); - GraphicSymbol parent=base.map(id->(GraphicSymbol)g.buildSymbol(g.index.byId(id).orElseThrow())).orElse(null); - var basis=syntax.basis().flatMap(n->g.bindTarget(decl,n,Set.of(SymbolKind.CLASS,SymbolKind.VIEW),ReferenceUsage.TYPE_USE,new ArrayList<>(),false)); + var base=syntax.baseGraphic().flatMap(n->names.bindTarget(decl,n,Set.of(SymbolKind.GRAPHIC),ReferenceUsage.BASE_TYPE,new ArrayList<>(),false)); + GraphicSymbol parent=base.map(id->(GraphicSymbol)ensureSymbol.apply(index.byId(id).orElseThrow())).orElse(null); + var basis=syntax.basis().flatMap(n->names.bindTarget(decl,n,Set.of(SymbolKind.CLASS,SymbolKind.VIEW),ReferenceUsage.TYPE_USE,new ArrayList<>(),false)); if(basis.isEmpty() && parent!=null) basis=parent.basis(); - if(basis.isEmpty()) g.emitRule(decl,"graphic requires a bound class or view basis","GRAPHIC_BASIS"); + if(basis.isEmpty()) diagnostics.emitRule(decl,"graphic requires a bound class or view basis","GRAPHIC_BASIS"); if(parent!=null) { - if(parent.header().has(Modifier.FINAL)) g.emitRule(decl,"cannot extend FINAL graphic","GRAPHIC_FINAL"); + if(parent.header().has(Modifier.FINAL)) diagnostics.emitRule(decl,"cannot extend FINAL graphic","GRAPHIC_FINAL"); if(basis.isPresent() && parent.basis().isPresent()) { - g.metadata.ensureBases(basis.get()); - if(!guru.interlis.model.internal.TypeCompatibility.specializes(basis.get(),parent.basis().get(),g.baseOf::get)) - g.emitRule(decl,"graphic basis must specialize inherited basis","GRAPHIC_BASIS"); + metadata.ensureBases(basis.get()); + if(!guru.interlis.model.internal.TypeCompatibility.specializes(basis.get(),parent.basis().get(),state::baseOf)) + diagnostics.emitRule(decl,"graphic basis must specialize inherited basis","GRAPHIC_BASIS"); } } - if(syntax.endName().filter(n->!n.equals(decl.name())).isPresent()) g.emitRule(decl,"graphic END name mismatch","GRAPHIC_END_NAME"); - basis.ifPresent(id->{ g.index.byId(id).ifPresent(g::buildSymbol); contexts.put(decl.id(),id); }); + if(syntax.endName().filter(n->!n.equals(decl.name())).isPresent()) diagnostics.emitRule(decl,"graphic END name mismatch","GRAPHIC_END_NAME"); + basis.ifPresent(id->{ index.byId(id).ifPresent(ensureSymbol::apply); contexts.put(decl.id(),id); }); List selections=new ArrayList<>(parent==null?List.of():parent.selections()); for(var selection:syntax.selections()) selections.add(condition(decl,selection)); LinkedHashMap effective=new LinkedHashMap<>(); - if(parent!=null) for(var id:parent.effectiveRules()) effective.put(g.symbols.get(id).name().value(),id); + if(parent!=null) for(var id:parent.effectiveRules()) effective.put(state.symbol(id).name().value(),id); List local=new ArrayList<>(); Set names=new HashSet<>(); - for(var child:g.index.children(decl.id())) { - g.checkWork(); + for(var child:index.children(decl.id())) { + work.checkpoint(); basis.ifPresent(id->contexts.put(child.id(),id)); - if(!names.add(child.name())) g.emitRule(child,"duplicate drawing rule","GRAPHIC_RULE_NAME"); + if(!names.add(child.name())) diagnostics.emitRule(child,"duplicate drawing rule","GRAPHIC_RULE_NAME"); var rule=rule(child,Optional.ofNullable(effective.get(child.name()))); - g.symbols.put(rule.id(),rule); local.add(rule.id()); effective.put(child.name(),rule.id()); + state.defineSymbol(rule); local.add(rule.id()); effective.put(child.name(),rule.id()); } if(!decl.modifiers().contains(Modifier.ABSTRACT)) for(var id:effective.values()) { - var rule=(DrawingRuleSymbol)g.symbols.get(id); - if(rule.header().has(Modifier.ABSTRACT)) g.emitRule(decl,"concrete graphic retains abstract drawing rule","GRAPHIC_ABSTRACT_RULE"); + var rule=(DrawingRuleSymbol)state.symbol(id); + if(rule.header().has(Modifier.ABSTRACT)) diagnostics.emitRule(decl,"concrete graphic retains abstract drawing rule","GRAPHIC_ABSTRACT_RULE"); } - var result=new GraphicSymbol(g.header(decl),base,basis,selections,local,List.copyOf(effective.values())); - base.ifPresent(id->g.baseOf.put(decl.id(),id)); building.remove(decl.id()); return result; + var result=new GraphicSymbol(symbolHeader.apply(decl),base,basis,selections,local,List.copyOf(effective.values())); + base.ifPresent(id->inheritance.bindBase(decl.id(),id)); building.remove(decl.id()); return result; } private ExpressionId condition(DeclInfo decl,ExpressionSyntax syntax) { - var result=g.buildExpression(syntax,decl.id()); - if(!result.error() && !g.isBoolean(result.typeId())) g.emitRule(decl,"graphic condition must be Boolean","GRAPHIC_CONDITION"); + var result=expressions.get().buildExpression(syntax,decl.id()); + if(!result.error() && !typeBindings.isBoolean(result.typeId())) diagnostics.emitRule(decl,"graphic condition must be Boolean","GRAPHIC_CONDITION"); return result.id(); } private DrawingRuleSymbol rule(DeclInfo decl,Optional inherited) { var syntax=(GraphicDeclarationSyntax.Rule)decl.syntax(); - var parent=inherited.map(id->(DrawingRuleSymbol)g.symbols.get(id)).orElse(null); + var parent=inherited.map(id->(DrawingRuleSymbol)state.symbol(id)).orElse(null); if(inherited.isPresent()!=decl.modifiers().contains(Modifier.EXTENDED)) - g.emitRule(decl,"drawing-rule override requires inherited rule and EXTENDED","GRAPHIC_RULE_OVERRIDE"); - if(parent!=null && parent.header().has(Modifier.FINAL)) g.emitRule(decl,"cannot override FINAL rule","GRAPHIC_RULE_FINAL"); - var signature=syntax.signatureClass().flatMap(n->g.bindTarget(decl,n,Set.of(SymbolKind.CLASS),ReferenceUsage.TYPE_USE,new ArrayList<>(),false)); + diagnostics.emitRule(decl,"drawing-rule override requires inherited rule and EXTENDED","GRAPHIC_RULE_OVERRIDE"); + if(parent!=null && parent.header().has(Modifier.FINAL)) diagnostics.emitRule(decl,"cannot override FINAL rule","GRAPHIC_RULE_FINAL"); + var signature=syntax.signatureClass().flatMap(n->names.bindTarget(decl,n,Set.of(SymbolKind.CLASS),ReferenceUsage.TYPE_USE,new ArrayList<>(),false)); if(signature.isEmpty() && parent!=null) signature=parent.signatureClass(); - if(signature.isEmpty() && !decl.modifiers().contains(Modifier.ABSTRACT)) g.emitRule(decl,"concrete drawing rule requires signature class","GRAPHIC_SIGNATURE"); + if(signature.isEmpty() && !decl.modifiers().contains(Modifier.ABSTRACT)) diagnostics.emitRule(decl,"concrete drawing rule requires signature class","GRAPHIC_SIGNATURE"); signature.ifPresent(id->{ ensureBuilt(id); - var sign=g.metadata.predefined(decl,"INTERLIS.SIGN",Set.of(SymbolKind.CLASS)).orElseThrow(); - if(!guru.interlis.model.internal.TypeCompatibility.specializes(id,sign,g.baseOf::get)) g.emitRule(decl,"drawing rule requires a SIGN class","GRAPHIC_SIGNATURE"); - if(parent!=null && parent.signatureClass().isPresent() && !guru.interlis.model.internal.TypeCompatibility.specializes(id,parent.signatureClass().get(),g.baseOf::get)) - g.emitRule(decl,"drawing signature must specialize base signature","GRAPHIC_SIGNATURE"); + var sign=metadata.predefined(decl,"INTERLIS.SIGN",Set.of(SymbolKind.CLASS)).orElseThrow(); + if(!guru.interlis.model.internal.TypeCompatibility.specializes(id,sign,state::baseOf)) diagnostics.emitRule(decl,"drawing rule requires a SIGN class","GRAPHIC_SIGNATURE"); + if(parent!=null && parent.signatureClass().isPresent() && !guru.interlis.model.internal.TypeCompatibility.specializes(id,parent.signatureClass().get(),state::baseOf)) + diagnostics.emitRule(decl,"drawing signature must specialize base signature","GRAPHIC_SIGNATURE"); }); List clauses=new ArrayList<>(); for(var clause:syntax.clauses()) { - g.checkWork(); + work.checkpoint(); var condition=clause.condition().map(e->condition(decl,e)); List assignments=new ArrayList<>(); for(var assignment:clause.assignments()) { - var parameter=signature.flatMap(id->g.metadata.parameter(id,assignment.parameter().text())); - g.recordReference(decl,assignment.parameter(),parameter.orElse(null),ReferenceUsage.EXPRESSION); - if(parameter.isEmpty()) g.emitRule(decl,"unknown signature parameter: "+assignment.parameter().text(),"GRAPHIC_PARAMETER"); + var parameter=signature.flatMap(id->metadata.parameter(id,assignment.parameter().text())); + references.recordReference(decl,assignment.parameter(),parameter.orElse(null),ReferenceUsage.EXPRESSION); + if(parameter.isEmpty()) diagnostics.emitRule(decl,"unknown signature parameter: "+assignment.parameter().text(),"GRAPHIC_PARAMETER"); assignments.add(new DrawingRuleSymbol.Assignment(assignment.range(),parameter,value(decl,parameter,assignment.value()))); } clauses.add(new DrawingRuleSymbol.Clause(clause.range(),condition,assignments)); } - var result=new DrawingRuleSymbol(g.header(decl),signature,inherited,clauses); + var result=new DrawingRuleSymbol(symbolHeader.apply(decl),signature,inherited,clauses); return result; } private void ensureBuilt(SymbolId id) { Set seen=new HashSet<>(); - while(id!=null && seen.add(id)) { g.checkWork(); g.index.byId(id).ifPresent(g::buildSymbol); id=g.baseOf.get(id); } + while(id!=null && seen.add(id)) { work.checkpoint(); index.byId(id).ifPresent(ensureSymbol::apply); id=state.baseOf(id); } } private List parameters(SymbolId owner) { LinkedHashMap result=new LinkedHashMap<>(); Set seen=new HashSet<>(); while(owner!=null && seen.add(owner)) { - g.checkWork(); var s=g.symbols.get(owner); - if(s instanceof ClassSymbol c) for(var id:c.members()) if(g.symbols.get(id) instanceof ParameterSymbol p) result.putIfAbsent(p.name().value(),id); - owner=g.baseOf.get(owner); + work.checkpoint(); var s=state.symbol(owner); + if(s instanceof ClassSymbol c) for(var id:c.members()) if(state.symbol(id) instanceof ParameterSymbol p) result.putIfAbsent(p.name().value(),id); + owner=state.baseOf(owner); } return List.copyOf(result.values()); } private void clauses(DrawingRuleSymbol rule, List result, Set seen) { if (!seen.add(rule.id())) return; - g.checkWork(); - rule.overrides().ifPresent(id -> clauses((DrawingRuleSymbol)g.symbols.get(id), result, seen)); + work.checkpoint(); + rule.overrides().ifPresent(id -> clauses((DrawingRuleSymbol)state.symbol(id), result, seen)); result.addAll(rule.clauses()); } @@ -122,11 +167,11 @@ private Set> assignmentContexts(DrawingRuleSy Set> result = new LinkedHashSet<>(); result.add(assignmentContext(clauses, -1, -1)); for (int i = 0; i < clauses.size(); i++) { - g.checkWork(); if (clauses.get(i).condition().isEmpty()) continue; + work.checkpoint(); if (clauses.get(i).condition().isEmpty()) continue; result.add(assignmentContext(clauses, i, -1)); if (hasSign(clauses.get(i))) continue; for (int j = 0; j < i; j++) { - g.checkWork(); + work.checkpoint(); if (clauses.get(j).condition().isPresent() && hasSign(clauses.get(j))) result.add(assignmentContext(clauses, j, i)); } @@ -136,8 +181,8 @@ private Set> assignmentContexts(DrawingRuleSy private boolean hasSign(DrawingRuleSymbol.Clause clause) { for (var assignment : clause.assignments()) { - g.checkWork(); - if (assignment.parameter().map(g.symbols::get).filter(p -> p.name().value().equals("Sign")).isPresent()) return true; + work.checkpoint(); + if (assignment.parameter().map(state::symbol).filter(p -> p.name().value().equals("Sign")).isPresent()) return true; } return false; } @@ -145,17 +190,17 @@ private boolean hasSign(DrawingRuleSymbol.Clause clause) { private List assignmentContext(List clauses, int first, int second) { Map values = new LinkedHashMap<>(); for (int i = 0; i < clauses.size(); i++) { - g.checkWork(); var clause = clauses.get(i); + work.checkpoint(); var clause = clauses.get(i); if (clause.condition().isPresent() && i != first && i != second) continue; for (var assignment : clause.assignments()) { - g.checkWork(); assignment.parameter().ifPresent(id -> values.put(g.symbols.get(id).name().value(), assignment)); + work.checkpoint(); assignment.parameter().ifPresent(id -> values.put(state.symbol(id).name().value(), assignment)); } } return List.copyOf(values.values()); } private void objects(DrawingRuleSymbol.Value value, Set result) { - g.checkWork(); + work.checkpoint(); if (value instanceof DrawingRuleSymbol.MetaObjectValue object) object.object().ifPresent(result::add); else if (value instanceof DrawingRuleSymbol.AccordingValue according) for (var entry : according.cases()) objects(entry.value(), result); @@ -169,23 +214,23 @@ private void responsibility(DeclInfo decl, DrawingRuleSymbol rule) { var contexts = assignmentContexts(rule); Set assigned = new HashSet<>(); for (var context : contexts) for (var assignment : context) { - g.checkWork(); assigned.add(g.symbols.get(assignment.parameter().orElseThrow()).name().value()); + work.checkpoint(); assigned.add(state.symbol(assignment.parameter().orElseThrow()).name().value()); } - if (!assigned.contains("Sign")) g.emitRule(decl, "missing signature parameter: Sign", "GRAPHIC_PARAMETER_MISSING"); + if (!assigned.contains("Sign")) diagnostics.emitRule(decl, "missing signature parameter: Sign", "GRAPHIC_PARAMETER_MISSING"); for (var used : contexts) checkContext(decl, declared, used, assigned); } private void checkContext(DeclInfo decl, SymbolId declared, List used, Set assigned) { Set signs = new LinkedHashSet<>(); boolean signExpression = false; for (var assignment : used) { - g.checkWork(); - var original = g.symbols.get(assignment.parameter().orElseThrow()); + work.checkpoint(); + var original = state.symbol(assignment.parameter().orElseThrow()); // Explicit METAOBJECT OF restrictions belong to the declared parameter // contract, even when its value was inherited and responsibility stays // at a base class. They must never be weakened by contextual promotion. - g.metadata.parameter(declared, original.name().value()).filter(id -> !id.equals(original.id())) - .map(g.symbols::get).map(ParameterSymbol.class::cast) - .filter(p -> g.effectiveDescriptor(p.typeId()) instanceof MetaObjectType) + metadata.parameter(declared, original.name().value()).filter(id -> !id.equals(original.id())) + .map(state::symbol).map(ParameterSymbol.class::cast) + .filter(p -> typeBindings.effectiveDescriptor(p.typeId()) instanceof MetaObjectType) .ifPresent(p -> checkValue(decl, p, assignment.value())); if (original.name().value().equals("Sign")) { objects(assignment.value(), signs); signExpression |= assignment.value() instanceof DrawingRuleSymbol.ExpressionValue; @@ -193,19 +238,19 @@ private void checkContext(DeclInfo decl, SymbolId declared, List responsibleClasses = new LinkedHashSet<>(); for (var object : signs) { - g.checkWork(); - var actual = ((MetaObjectSymbol)g.symbols.get(object)).objectClass().orElse(null); + work.checkpoint(); + var actual = ((MetaObjectSymbol)state.symbol(object)).objectClass().orElse(null); if (actual == null) continue; if (!specializes(actual, declared) && !specializes(declared, actual)) { - g.emitRule(decl, "metadata object class does not match signature parameter", "GRAPHIC_METAOBJECT_TYPE"); + diagnostics.emitRule(decl, "metadata object class does not match signature parameter", "GRAPHIC_METAOBJECT_TYPE"); continue; } SymbolId responsible = actual; for (var assignment : used) { - g.checkWork(); var owner = g.symbols.get(assignment.parameter().orElseThrow()).owner().orElseThrow(); + work.checkpoint(); var owner = state.symbol(assignment.parameter().orElseThrow()).owner().orElseThrow(); if (specializes(responsible, owner)) continue; if (specializes(owner, responsible) && specializes(declared, owner)) responsible = owner; - else g.emitRule(decl, "signature parameter owner cannot determine a compatible responsible class", "GRAPHIC_SIGNATURE_RESPONSIBILITY"); + else diagnostics.emitRule(decl, "signature parameter owner cannot determine a compatible responsible class", "GRAPHIC_SIGNATURE_RESPONSIBILITY"); } responsibleClasses.add(responsible); } @@ -213,24 +258,24 @@ private void checkContext(DeclInfo decl, SymbolId declared, List targets = new LinkedHashSet<>(); objects(assignment.value(), targets); for (SymbolId object : targets) { - g.checkWork(); var actual = ((MetaObjectSymbol)g.symbols.get(object)).objectClass(); + work.checkpoint(); var actual = ((MetaObjectSymbol)state.symbol(object)).objectClass(); if (actual.isPresent() && !specializes(actual.orElseThrow(), responsible)) - g.emitRule(decl, "metadata object class does not match signature parameter", "GRAPHIC_METAOBJECT_TYPE"); + diagnostics.emitRule(decl, "metadata object class does not match signature parameter", "GRAPHIC_METAOBJECT_TYPE"); } } } @@ -238,58 +283,58 @@ private void checkContext(DeclInfo decl, SymbolId declared, List parameter,GraphicDeclarationSyntax.Value syntax) { - TypeDescriptor expected=parameter.map(g.symbols::get).map(ParameterSymbol.class::cast).map(p->g.effectiveDescriptor(p.typeId())).orElse(null); + TypeDescriptor expected=parameter.map(state::symbol).map(ParameterSymbol.class::cast).map(p->typeBindings.effectiveDescriptor(p.typeId())).orElse(null); if(syntax instanceof GraphicDeclarationSyntax.MetaObjectValue m) { - var target=g.metadata.objectReference(decl,m.object()); - if(expected!=null && !(expected instanceof MetaObjectType)) g.emitRule(decl,"metadata object assigned to an ordinary parameter","GRAPHIC_PARAMETER_TYPE"); + var target=metadata.objectReference(decl,m.object()); + if(expected!=null && !(expected instanceof MetaObjectType)) diagnostics.emitRule(decl,"metadata object assigned to an ordinary parameter","GRAPHIC_PARAMETER_TYPE"); if(expected instanceof MetaObjectType type) { var required=type.target(); - var actual=target.map(g.symbols::get).filter(MetaObjectSymbol.class::isInstance).map(MetaObjectSymbol.class::cast).flatMap(MetaObjectSymbol::objectClass); - if(actual.isPresent() && required.isPresent() && !guru.interlis.model.internal.TypeCompatibility.specializes(actual.get(),required.get(),g.baseOf::get)) - g.emitRule(decl,"metadata object class does not match signature parameter","GRAPHIC_METAOBJECT_TYPE"); + var actual=target.map(state::symbol).filter(MetaObjectSymbol.class::isInstance).map(MetaObjectSymbol.class::cast).flatMap(MetaObjectSymbol::objectClass); + if(actual.isPresent() && required.isPresent() && !guru.interlis.model.internal.TypeCompatibility.specializes(actual.get(),required.get(),state::baseOf)) + diagnostics.emitRule(decl,"metadata object class does not match signature parameter","GRAPHIC_METAOBJECT_TYPE"); } return new DrawingRuleSymbol.MetaObjectValue(target); } if(syntax instanceof GraphicDeclarationSyntax.ExpressionValue e) { - var expression=g.buildExpression(e.expression(),decl.id()); - if(expected!=null)expression=g.contextualValue(expression,expected.id(),decl.id()); - if(!expression.error() && parameter.isPresent())checkMandatoryValue(decl,(ParameterSymbol)g.symbols.get(parameter.orElseThrow()),g.expressions.get(expression.id())); - if(expected!=null && !expression.error() && !g.assignable(expression.typeId(),expected.id())) - g.emitRule(decl,"incompatible signature parameter assignment","GRAPHIC_PARAMETER_TYPE"); + var expression=expressions.get().buildExpression(e.expression(),decl.id()); + if(expected!=null)expression=expressions.get().contextualValue(expression,expected.id(),decl.id()); + if(!expression.error() && parameter.isPresent())checkMandatoryValue(decl,(ParameterSymbol)state.symbol(parameter.orElseThrow()),state.expression(expression.id())); + if(expected!=null && !expression.error() && !typeBindings.assignable(expression.typeId(),expected.id())) + diagnostics.emitRule(decl,"incompatible signature parameter assignment","GRAPHIC_PARAMETER_TYPE"); return new DrawingRuleSymbol.ExpressionValue(expression.id()); } var according=(GraphicDeclarationSyntax.AccordingValue)syntax; - var selector=g.buildExpression(according.selector(),decl.id()); - var type=g.effectiveDescriptor(selector.typeId()); - if(!(type instanceof EnumerationType)) g.emitRule(decl,"ACCORDING requires an enumeration attribute","GRAPHIC_ENUM_SELECTOR"); + var selector=expressions.get().buildExpression(according.selector(),decl.id()); + var type=typeBindings.effectiveDescriptor(selector.typeId()); + if(!(type instanceof EnumerationType)) diagnostics.emitRule(decl,"ACCORDING requires an enumeration attribute","GRAPHIC_ENUM_SELECTOR"); List cases=new ArrayList<>(); for(var entry:according.cases()) { if(type instanceof EnumerationType enumeration) { @@ -299,7 +344,7 @@ private DrawingRuleSymbol.Value value(DeclInfo decl,Optional parameter var last=entry.last().get(); valid &= enumeration.ordered() && paths.contains(last) && parent(entry.first()).equals(parent(last)) && paths.indexOf(entry.first())<=paths.indexOf(last); } - if(!valid) g.emitRule(decl,"invalid enumeration node or ordered sibling range","GRAPHIC_ENUM_RANGE"); + if(!valid) diagnostics.emitRule(decl,"invalid enumeration node or ordered sibling range","GRAPHIC_ENUM_RANGE"); } cases.add(new DrawingRuleSymbol.EnumAssignment(entry.range(),entry.first(),entry.last(),value(decl,parameter,entry.value()))); } @@ -308,13 +353,13 @@ private DrawingRuleSymbol.Value value(DeclInfo decl,Optional parameter private String parent(String path) { int dot=path.lastIndexOf('.'); return dot<0?"":path.substring(0,dot); } void validateDependencies() { - for(var symbol:g.symbols.values()) if(symbol instanceof GraphicSymbol graphic && graphic.basis().isPresent()) { - var decl=g.index.byId(graphic.id()).orElseThrow(); - for (var id : graphic.rules()) responsibility(g.index.byId(id).orElseThrow(), (DrawingRuleSymbol)g.symbols.get(id)); - var source=g.scopeRoot(graphic.id()); var target=g.scopeRoot(graphic.basis().get()); - if(g.symbols.get(source) instanceof TopicSymbol && g.symbols.get(target) instanceof TopicSymbol - && !g.isTopicSubtype(source,target) && !g.topicDependencies(source).contains(target)) - g.emitRule(decl,"graphic basis topic requires DEPENDS ON","GRAPHIC_TOPIC_DEPENDENCY"); + for(var symbol:state.symbolsInBuildOrder()) if(symbol instanceof GraphicSymbol graphic && graphic.basis().isPresent()) { + var decl=index.byId(graphic.id()).orElseThrow(); + for (var id : graphic.rules()) responsibility(index.byId(id).orElseThrow(), (DrawingRuleSymbol)state.symbol(id)); + var source=scopeRoot.apply(graphic.id()); var target=scopeRoot.apply(graphic.basis().get()); + if(state.symbol(source) instanceof TopicSymbol && state.symbol(target) instanceof TopicSymbol + && !topics.isTopicSubtype(source,target) && !topics.topicDependencies(source).contains(target)) + diagnostics.emitRule(decl,"graphic basis topic requires DEPENDS ON","GRAPHIC_TOPIC_DEPENDENCY"); } } } diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/IndexedDeclarations.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/IndexedDeclarations.java index f37e58b4..44927b4f 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/IndexedDeclarations.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/IndexedDeclarations.java @@ -1,6 +1,7 @@ package guru.interlis.compiler.internal; import guru.interlis.model.SymbolId; +import guru.interlis.source.SourceId; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -36,6 +37,15 @@ List models() { return models; } + Optional findModelId(String name, int offset, SourceId source) { + return models.stream() + .filter(e -> e.model().sourceId().equals(source)) + .filter(e -> e.model().name().equals(name)) + .filter(e -> e.model().declarationOffset() == offset) + .map(ModelEntry::id) + .findFirst(); + } + Optional byId(SymbolId id) { return Optional.ofNullable(byId.get(id)); } diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/InheritanceBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/InheritanceBindings.java new file mode 100644 index 00000000..3b65a496 --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/InheritanceBindings.java @@ -0,0 +1,672 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.diagnostics.DiagnosticArgument; +import guru.interlis.diagnostics.DiagnosticCode; +import guru.interlis.diagnostics.DiagnosticDraft; +import guru.interlis.diagnostics.Phase; +import guru.interlis.diagnostics.Severity; +import guru.interlis.model.AssociationSymbol; +import guru.interlis.model.AttributeSymbol; +import guru.interlis.model.ClassSymbol; +import guru.interlis.model.DomainReferenceType; +import guru.interlis.model.EffectiveMember; +import guru.interlis.model.EffectiveMemberOrigin; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.RoleAggregation; +import guru.interlis.model.RoleSymbol; +import guru.interlis.model.StructureSymbol; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TopicSymbol; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.syntax.ClassDeclarationSyntax; +import guru.interlis.syntax.StructureDeclarationSyntax; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Binds inheritance and effective members in their declared order. */ +final class InheritanceBindings { + private final SemanticGraphRead.Inheritance state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameBindings names; + private final Supplier typeBindings; + private final TopicBindings topics; + private final Supplier metadata; + private final Consumer replaceOverride; + private final Function scopeRoot; + private final Function ensureSymbol; + private final Set resolvedObjectBases = new HashSet<>(); + private final Set effectiveProcessed = new HashSet<>(); + + InheritanceBindings(SemanticGraphRead.Inheritance state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameBindings names, + Supplier typeBindings, + TopicBindings topics, + Supplier metadata, + Consumer replaceOverride, + Function scopeRoot, + Function ensureSymbol) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.names = names; + this.typeBindings = typeBindings; + this.topics = topics; + this.metadata = metadata; + this.replaceOverride = replaceOverride; + this.scopeRoot = scopeRoot; + this.ensureSymbol = ensureSymbol; + } + + Optional baseTopicMember(SymbolId context, String name, + Set expected) { + SymbolId current = scopeRoot.apply(context); + Set seen = new HashSet<>(); + while (current != null && seen.add(current)) { + work.checkpoint(); + DeclInfo topicDecl = index.byId(current).orElse(null); + if (topicDecl == null || !(topicDecl.syntax() instanceof guru.interlis.syntax.TopicSyntax syntax) + || syntax.baseTopic().isEmpty()) { + return Optional.empty(); + } + Optional base = names.tryResolve(topicDecl, syntax.baseTopic().orElseThrow(), + Set.of(SymbolKind.TOPIC)); + if (base.isEmpty()) { + return Optional.empty(); + } + for (DeclInfo child : index.children(base.orElseThrow())) { + work.checkpoint(); + if (expected.contains(child.kind()) && child.name().equals(name)) { + return Optional.of(child.id()); + } + } + current = base.orElseThrow(); + } + return Optional.empty(); + } + + /** Import pairs follow their bound targets, never the available language variants. */ + void validateClassExtensions() { + Set extendedBases = new LinkedHashSet<>(); + Map> subclasses = new LinkedHashMap<>(); + for (Symbol symbol : state.symbolsInBuildOrder()) { + work.checkpoint(); + if (!(symbol instanceof ClassSymbol clazz) || clazz.baseType().isEmpty()) { + continue; + } + SymbolId base = clazz.baseType().orElseThrow(); + subclasses.computeIfAbsent(base, ignored -> new ArrayList<>()).add(clazz.id()); + Symbol baseSymbol = state.symbol(base); + if (baseSymbol != null && baseSymbol.header().modifiers() + .contains(guru.interlis.model.Modifier.EXTENDED)) { + extendedBases.add(base); + } + } + Map extendedByTopic = new LinkedHashMap<>(); + for (Symbol symbol : state.symbolsInBuildOrder()) { + work.checkpoint(); + if (symbol instanceof ClassSymbol clazz + && clazz.header().modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + SymbolId topic = scopeRoot.apply(clazz.id()); + if (topic != null) { + extendedByTopic.put(topic + ":" + clazz.name().value(), clazz.id()); + } + } + } + for (Symbol symbol : state.symbolsInBuildOrder()) { + work.checkpoint(); + if (!(symbol instanceof ClassSymbol clazz) || clazz.baseType().isEmpty()) { + continue; + } + SymbolId base = clazz.baseType().orElseThrow(); + Symbol baseSymbol = state.symbol(base); + SymbolId topic = scopeRoot.apply(clazz.id()); + if (baseSymbol != null && topic != null) { + SymbolId localExtended = extendedByTopic.get(topic + ":" + baseSymbol.name().value()); + if (localExtended != null && !localExtended.equals(base) && !localExtended.equals(clazz.id())) { + index.byId(clazz.id()).ifPresent(decl -> + diagnostics.emitRule(decl, "subclass must extend the local EXTENDED class", + "EXTENDED_BASE_CLASS")); + } else if (localExtended != null + && state.symbol(topic) instanceof TopicSymbol topicSymbol + && topicSymbol.baseTopic().isEmpty()) { + index.byId(clazz.id()).ifPresent(decl -> + diagnostics.emitRule(decl, "EXTENDED class may only be extended in a derived topic", + "EXTENDED_BASE_CLASS")); + } + } + } + for (Symbol symbol : state.symbolsInBuildOrder()) { + work.checkpoint(); + if (!(symbol instanceof ClassSymbol clazz) + || !clazz.header().modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + continue; + } + SymbolId topic = scopeRoot.apply(clazz.id()); + if (!(state.symbol(topic) instanceof TopicSymbol topicSymbol) + || topicSymbol.baseTopic().isEmpty()) { + continue; + } + SymbolId baseTopic = topicSymbol.baseTopic().orElseThrow(); + for (DeclInfo baseChild : index.children(baseTopic)) { + work.checkpoint(); + boolean specialized = index.children(baseTopic).stream() + .filter(child -> child.kind() == SymbolKind.CLASS) + .anyMatch(child -> state.baseOf(child.id()) != null + && state.baseOf(child.id()).equals(baseChild.id())); + if (baseChild.kind() == SymbolKind.CLASS + && baseChild.name().equals(clazz.name().value()) && specialized) { + index.byId(clazz.id()).ifPresent(decl -> + diagnostics.emitRule(decl, "EXTENDED class conflicts with a specialized base class", + "EXTENDED_CLASS_CONFLICT")); + } + } + } + } + + void validateAttributeOverrides() { + for (Symbol symbol : state.symbolsInBuildOrder()) { + work.checkpoint(); + if (!(symbol instanceof ClassSymbol) && !(symbol instanceof StructureSymbol) && !(symbol instanceof AssociationSymbol)) { + continue; + } + SymbolId base = state.baseOf(symbol.id()); + if (base == null) { + continue; + } + List members = symbol instanceof ClassSymbol classSymbol + ? classSymbol.members() : symbol instanceof StructureSymbol structure ? structure.members() + : ((AssociationSymbol) symbol).attributes(); + for (SymbolId member : members) { + work.checkpoint(); + Symbol memberSymbol = state.symbol(member); + if (!(memberSymbol instanceof AttributeSymbol attribute)) { + continue; + } + DeclInfo attributeDecl = index.byId(member).orElse(null); + if (attributeDecl == null + || !attributeDecl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + continue; + } + AttributeSymbol baseAttribute = findBaseAttribute(base, attributeDecl.name()); + if (baseAttribute == null) { + continue; + } + validateAttributeOverrideProperties(attributeDecl, attribute, baseAttribute); + TypeDescriptor baseType = typeBindings.get().effectiveDescriptor(baseAttribute.declaredType()); + TypeDescriptor derivedType = typeBindings.get().effectiveDescriptor(attribute.declaredType()); + if (baseType != null && derivedType != null) { + String problem = overrideProblem(baseType, derivedType, attributeDecl.language()); + if (problem != null) { + diagnostics.emitRule(attributeDecl, problem, "ATTRIBUTE_OVERRIDE_TYPE"); + } + } + } + } + } + + /** Shared mandatory, transient and domain-identity rules for attribute overrides. */ + void validateAttributeOverrideProperties(DeclInfo attributeDecl, AttributeSymbol attribute, AttributeSymbol baseAttribute) { + if (baseAttribute.mandatory() && !attribute.mandatory()) { + diagnostics.emitRule(attributeDecl, "extended attribute may not remove MANDATORY", + "ATTRIBUTE_OVERRIDE_MANDATORY"); + } + if (baseAttribute.transientAttribute() != attribute.transientAttribute()) { + diagnostics.emitRule(attributeDecl, "extended attribute changes TRANSIENT mode", + "ATTRIBUTE_OVERRIDE_TRANSIENT"); + } + if (state.type(baseAttribute.declaredType()) instanceof DomainReferenceType baseDomainRef + && state.type(attribute.declaredType()) instanceof DomainReferenceType derivedDomainRef + && !domainExtends(derivedDomainRef.targetDomain(), baseDomainRef.targetDomain())) { + diagnostics.emitRule(attributeDecl, "extended attribute changes domain", + "ATTRIBUTE_OVERRIDE_TYPE"); + } + } + + AttributeSymbol findBaseAttribute(SymbolId base, String name) { + SymbolId current = base; + Set seen = new HashSet<>(); + while (current != null && seen.add(current)) { + work.checkpoint(); + Symbol symbol = state.symbol(current); + List members = symbol instanceof ClassSymbol classSymbol + ? classSymbol.members() + : symbol instanceof StructureSymbol structure ? structure.members() + : symbol instanceof AssociationSymbol association ? association.attributes() : List.of(); + for (SymbolId member : members) { + work.checkpoint(); + Symbol memberSymbol = state.symbol(member); + if (memberSymbol instanceof AttributeSymbol attribute && attribute.name().value().equals(name)) { + return attribute; + } + } + current = state.baseOf(current); + } + return null; + } + + boolean domainExtends(SymbolId derived, SymbolId base) { + SymbolId current = derived; + Set visited = new LinkedHashSet<>(); + while (current != null && visited.add(current)) { + work.checkpoint(); + if (current.equals(base)) { + return true; + } + current = state.baseOf(current); + } + return false; + } + + private String overrideProblem(TypeDescriptor base, TypeDescriptor derived, guru.interlis.model.LanguageVersion language) { + var relation=typeBindings.get().extension(derived,base,language); + return relation.decision()==guru.interlis.model.RelationDecision.ALLOWED?null:relation.reason(); + } + + void validateAssociationRules() { + for (Symbol symbol : state.symbolsInBuildOrder()) { + work.checkpoint(); + if (symbol instanceof AssociationSymbol association) { + Set roleNames = new LinkedHashSet<>(); + for (SymbolId roleId : association.roles()) { + work.checkpoint(); + Symbol role = state.symbol(roleId); + if (role != null) { + roleNames.add(role.name().value()); + } + } + for (SymbolId memberId : association.attributes()) { + work.checkpoint(); + Symbol member = state.symbol(memberId); + if (member != null && roleNames.contains(member.name().value())) { + DeclInfo associationDecl = index.byId(association.id()).orElse(null); + if (associationDecl != null) { + diagnostics.emitRule(associationDecl, + "role and attribute share a name in the association", + "ROLE_ATTRIBUTE_CONFLICT"); + } + } + } + } + } + Map> roleNamesByTarget = new LinkedHashMap<>(); + for (AssociationSymbol association : state.symbolsInBuildOrder().stream() + .filter(AssociationSymbol.class::isInstance) + .map(AssociationSymbol.class::cast).toList()) { + work.checkpoint(); + DeclInfo decl = index.byId(association.id()).orElse(null); + if (decl == null) { + continue; + } + guru.interlis.syntax.AssociationDeclarationSyntax syntax = + (guru.interlis.syntax.AssociationDeclarationSyntax) decl.syntax(); + List effectiveRoles = associationRoles(association.id()); + if (effectiveRoles.size() < 2) diagnostics.emitRule(decl, "association requires at least two effective roles", "ASSOCIATION_ROLES"); + for (RoleSymbol role : effectiveRoles) { + if (role.targetRoots().stream().anyMatch(t -> targetSpecializes(association.id(), t) + && index.byId(t).map(DeclInfo::kind).orElse(null) == SymbolKind.ASSOCIATION)) { + diagnostics.emitRule(decl, "association may not specialize a role target", "ASSOCIATION_TARGET_BASE"); + } + } + for (RoleSymbol hiding : effectiveRoles) { + if (!hiding.hiding()) continue; + for (Symbol other : state.symbolsInBuildOrder()) { + work.checkpoint(); + if (!(other instanceof AssociationSymbol sibling) + || baseChain(association.id()).contains(sibling.id()) + || baseChain(sibling.id()).contains(association.id()) + || baseChain(association.id()).stream().noneMatch(baseChain(sibling.id())::contains)) continue; + for (RoleSymbol role : associationRoles(sibling.id())) { + if (!role.hiding() && role.name().equals(hiding.name()) + && role.targetRoots().stream().anyMatch(t -> hiding.targetRoots().stream() + .anyMatch(h -> targetSpecializes(t, h) || targetSpecializes(h, t)))) { + diagnostics.emitRule(decl, "HIDING conflicts with a non-hiding sibling role", "ROLE_HIDING_CONFLICT"); + } + } + } + } + long aggregations = effectiveRoles.stream() + .filter(role -> role.aggregation() != RoleAggregation.NONE).count(); + if (aggregations > 1) { + diagnostics.emitRule(decl, "only one aggregation role is allowed", "MULTIPLE_AGGREGATION"); + } + if (effectiveRoles.size()!=2 && effectiveRoles.stream().anyMatch(role->role.aggregation()==RoleAggregation.AGGREGATION)) { + diagnostics.emitRule(decl, "aggregation requires exactly two effective roles", "AGGREGATION_ARITY"); + } + boolean associationAbstract = syntax.modifiers() + .contains(guru.interlis.model.Modifier.ABSTRACT); + for (guru.interlis.syntax.RoleDeclarationSyntax roleSyntax : syntax.roles()) { + work.checkpoint(); + if (roleSyntax.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT) + && !associationAbstract) { + DeclInfo roleDecl = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.ROLE + && child.name().equals(roleSyntax.name())) + .findFirst().orElse(decl); + diagnostics.emitRule(roleDecl, "abstract role requires an abstract association", + "ABSTRACT_ROLE"); + } + if (roleSyntax.aggregation() != RoleAggregation.COMPOSITION + || !roleSyntax.explicitCardinality()) { + continue; + } + boolean one = roleSyntax.cardinality().lower().equals(java.math.BigInteger.ONE) + && roleSyntax.cardinality().upper() + instanceof guru.interlis.model.CardinalityUpper.Finite finite + && finite.value().equals(java.math.BigInteger.ONE); + if (!one) { + DeclInfo roleDecl = index.children(decl.id()).stream() + .filter(child -> child.kind() == SymbolKind.ROLE + && child.name().equals(roleSyntax.name())) + .findFirst().orElse(decl); + diagnostics.emitRule(roleDecl, "composition role cardinality must be 1", "COMPOSITION_CARDINALITY"); + } + } + List roles = association.roles().stream() + .map(state::symbol).filter(RoleSymbol.class::isInstance) + .map(RoleSymbol.class::cast).toList(); + if (roles.size() == 2) { + roles.get(0).targetRoots().forEach(t -> addRoleNamespace(roleNamesByTarget, t, roles.get(1))); + roles.get(1).targetRoots().forEach(t -> addRoleNamespace(roleNamesByTarget, t, roles.get(0))); + } + } + for (ClassSymbol classSymbol : state.symbolsInBuildOrder().stream() + .filter(ClassSymbol.class::isInstance).map(ClassSymbol.class::cast).toList()) { + work.checkpoint(); + Set chain = new LinkedHashSet<>(); + SymbolId current = classSymbol.id(); + while (current != null && chain.add(current)) { + work.checkpoint(); + current = state.baseOf(current); + } + Map attributes = new LinkedHashMap<>(); + for (SymbolId ancestor : chain) for (DeclInfo member : index.children(ancestor)) { + work.checkpoint(); + if (member.kind() == SymbolKind.ATTRIBUTE) attributes.putIfAbsent(member.name(), member); + } + for (SymbolId classId : chain) { + work.checkpoint(); + Map names = roleNamesByTarget.get(classId); + if (names == null) { + continue; + } + for (DeclInfo memberDecl : attributes.values()) { + work.checkpoint(); + DeclInfo role = names.get(memberDecl.name()); + if (role == null) continue; + DeclInfo association = role.owner().flatMap(index::byId).orElse(null); + DeclInfo attributeOwner = memberDecl.owner().flatMap(index::byId).orElse(null); + // A later association cannot retroactively add an inherited name to + // an already declared subclass. The direct target still owns its + // own namespace and is checked regardless of declaration order. + if (!memberDecl.owner().filter(classId::equals).isPresent() + && association != null && attributeOwner != null + && association.sourceId().equals(attributeOwner.sourceId()) + && association.syntax().range().startOffset() > attributeOwner.syntax().range().startOffset()) continue; + { + diagnostics.emitRule(memberDecl, "attribute name conflicts with role name", + "ATTRIBUTE_ROLE_CONFLICT"); + } + } + } + } + } + + private void addRoleNamespace(Map> namesByTarget, + SymbolId target, RoleSymbol role) { + DeclInfo roleDecl = index.byId(role.id()).orElse(null); + if (roleDecl == null) { + return; + } + Map names = namesByTarget.computeIfAbsent(target, + ignored -> new LinkedHashMap<>()); + DeclInfo previous = names.putIfAbsent(roleDecl.name(), roleDecl); + if (previous != null && !previous.id().equals(roleDecl.id())) { + SymbolId firstTopic = scopeRoot.apply(role.id()); + SymbolId secondTopic = scopeRoot.apply(previous.id()); + boolean sharedBase = firstTopic != null && secondTopic != null + && !firstTopic.equals(secondTopic) + && topics.shareBaseTopic(firstTopic, secondTopic); + SymbolId currentOwner = role.header().owner().orElseThrow(); + SymbolId previousOwner = previous.owner().orElseThrow(); + boolean sameFamily = baseChain(currentOwner).stream().anyMatch(baseChain(previousOwner)::contains); + if (!sharedBase && !sameFamily) { + diagnostics.emitRule(roleDecl, "role name conflicts with another role in the same namespace", + "ROLE_NAMESPACE_CONFLICT"); + } + } + } + + void validateTopicInheritanceCycles() { + for (Symbol symbol : state.symbolsInBuildOrder()) { + work.checkpoint(); + if (symbol instanceof TopicSymbol && hasCycle(baseChain(symbol.id()))) + index.byId(symbol.id()).ifPresent(this::emitCycle); + } + } + + /** Bind inheritance before members, including members reached from another source. */ + Optional objectInheritanceBase(SymbolId owner) { + DeclInfo decl = index.byId(owner).orElse(null); + if (decl == null || (decl.kind() != SymbolKind.CLASS && decl.kind() != SymbolKind.STRUCTURE) + || !resolvedObjectBases.add(owner)) return Optional.ofNullable(state.baseOf(owner)); + Optional name = decl.syntax() instanceof ClassDeclarationSyntax c + ? c.baseType() : ((StructureDeclarationSyntax) decl.syntax()).baseType(); + if (name.isPresent() && decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) + diagnostics.emitRule(decl, "EXTENDED and EXTENDS cannot be combined in one declaration", "EXTENDED_EXPLICIT_BASE"); + Set kinds = decl.kind() == SymbolKind.CLASS + ? Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE) : Set.of(SymbolKind.STRUCTURE); + Optional base=name.flatMap(n->names.bindTarget(decl,n,kinds,ReferenceUsage.BASE_TYPE,new ArrayList<>())); + + if (base.isEmpty() && decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + base = baseTopicMember(owner, decl.name(), Set.of(decl.kind())); + if (base.isEmpty()) diagnostics.emitRule(decl, "EXTENDED needs a same-named inherited declaration", "EXTENDED_BASE"); + } + base.ifPresent(id -> { + state.bindBase(owner, id); + if (isFinalDeclaration(id)) + diagnostics.emitRule(decl, "cannot extend FINAL object type", "OBJECT_FINAL"); + }); + return base; + } + + /** Declaration metadata is available before an imported base is built. */ + boolean isFinalDeclaration(SymbolId id) { + return index.byId(id).map(info -> info.modifiers().contains(guru.interlis.model.Modifier.FINAL)) + .orElseGet(() -> state.hasSymbol(id) && state.symbol(id).header().has(guru.interlis.model.Modifier.FINAL)); + } + + List associationRoles(SymbolId association) { + Map roles = new LinkedHashMap<>(); + List chain = baseChain(association); + for (int i = chain.size() - 1; i >= 0; i--) { + for (DeclInfo child : index.children(chain.get(i))) { + work.checkpoint(); + if (child.kind() == SymbolKind.ROLE) { + roles.put(child.name(), (RoleSymbol) ensureSymbol.apply(child)); + } + } + } + return List.copyOf(roles.values()); + } + + boolean targetSpecializes(SymbolId candidate, SymbolId base) { + return objectBase(base).isEmpty() + || guru.interlis.model.internal.TypeCompatibility.specializes(candidate, base, state::baseOf); + } + + boolean roleAccepts(RoleSymbol role, SymbolId candidate) { + return role.targetRoots().stream().anyMatch(base -> targetSpecializes(candidate, base)); + } + + Optional objectBase(SymbolId target) { + Symbol symbol = state.symbol(target); + return symbol != null && symbol.origin() instanceof Origin.PredefinedOrigin predefined + && predefined.key().equals("INTERLIS.ANYCLASS") ? Optional.empty() : Optional.of(target); + } + + void resolveOverridesAndEffectiveMembers() { + for (IndexedDeclarations.ModelEntry entry : index.models()) { + work.checkpoint(); + for (DeclInfo child : index.children(entry.id())) { + work.checkpoint(); + collectEffective(child); + } + } + } + + void collectEffective(DeclInfo decl) { + if (!effectiveProcessed.add(decl.id())) { + return; + } + if (decl.kind() == SymbolKind.CLASS || decl.kind() == SymbolKind.STRUCTURE + || decl.kind() == SymbolKind.ASSOCIATION) { + if (decl.kind() != SymbolKind.ASSOCIATION) metadata.get().ensureBases(decl.id()); + List chain = baseChain(decl.id()); + if (hasCycle(chain)) { + emitCycle(decl); + state.setEffectiveMembers(decl.id(), localMembers(decl)); + return; + } + List result = new ArrayList<>(); + Map positions = new HashMap<>(); + for (int depth = chain.size() - 1; depth >= 0; depth--) { + work.checkpoint(); + SymbolId current = chain.get(depth); + if (state.symbol(current) != null && state.symbol(current).origin() instanceof Origin.PredefinedOrigin) { + var predefined = state.symbol(current); + var members = predefined instanceof StructureSymbol s ? s.members() : predefined instanceof ClassSymbol c ? c.members() : List.of(); + for (SymbolId member : members) { + if (state.symbol(member).kind() != SymbolKind.ATTRIBUTE) continue; + String name = state.symbol(member).name().value(); + Integer existing = positions.get(name); + int position = existing == null ? result.size() : existing; + var effective = new EffectiveMember(member, decl.id(), pathTo(chain, depth), position, + EffectiveMemberOrigin.INHERITED, existing != null); + if (existing == null) { positions.put(name, position); result.add(effective); } + else result.set(position, effective); + } + } + for (DeclInfo attribute : index.children(current)) { + work.checkpoint(); + if (attribute.kind() != SymbolKind.ATTRIBUTE && attribute.kind() != SymbolKind.ROLE) { + continue; + } + ensureSymbol.apply(attribute); + Integer existing = positions.get(attribute.name()); + if (existing != null) { + if (depth == 0 && !attribute.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + diagnostics.emitRule(attribute, "override requires EXTENDED", "OVERRIDE"); + } + EffectiveMember old = result.get(existing); + if (depth == 0 && attribute.kind() == SymbolKind.ATTRIBUTE && isFinalDeclaration(old.declaredSymbol())) + diagnostics.emitRule(attribute, "cannot override FINAL attribute", "ATTRIBUTE_FINAL"); + result.set(existing, new EffectiveMember(attribute.id(), decl.id(), + pathTo(chain, depth), existing, + depth == 0 ? EffectiveMemberOrigin.OVERRIDDEN : EffectiveMemberOrigin.INHERITED, true)); + AttributeSymbol override = attribute.kind() == SymbolKind.ATTRIBUTE + ? overrideAttribute(attribute.id(), old.declaredSymbol()) : null; + if (override != null) { + replaceOverride.accept(override); + } + } else { + if (depth == 0 && attribute.kind() == SymbolKind.ATTRIBUTE + && attribute.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) + diagnostics.emitRule(attribute, "EXTENDED attribute needs an inherited attribute", "EXTENDED_BASE"); + int position = result.size(); + positions.put(attribute.name(), position); + result.add(new EffectiveMember(attribute.id(), decl.id(), + pathTo(chain, depth), position, + depth == 0 ? EffectiveMemberOrigin.LOCAL + : EffectiveMemberOrigin.INHERITED, false)); + } + } + } + state.setEffectiveMembers(decl.id(), List.copyOf(result)); + } + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + collectEffective(child); + } + } + + private AttributeSymbol overrideAttribute(SymbolId id, SymbolId baseId) { + if (state.symbol(id) instanceof AttributeSymbol attribute) { + return new AttributeSymbol(attribute.header(), attribute.declaredType(), + attribute.mandatory(), attribute.transientAttribute(), attribute.continuous(), + attribute.subdivision(), Optional.of(baseId), attribute.derivations()); + } + return null; + } + + List baseChain(SymbolId id) { + List chain = new ArrayList<>(); + SymbolId current = id; + Set seen = new HashSet<>(); + while (current != null) { + work.checkpoint(); + chain.add(current); + if (!seen.add(current)) break; + current = state.baseOf(current); + } + return chain; + } + + private boolean hasCycle(List chain) { + return chain.size() != new HashSet<>(chain).size(); + } + + private void emitCycle(DeclInfo decl) { + diagnostics.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0006"), Severity.ERROR, + Phase.SEMANTIC_CHECKING, "diagnostic.semantics.cycle") + .at(decl.origin() instanceof Origin.DeclaredOrigin d ? d.range() : decl.nameRange().orElse(null)) + .argument(new DiagnosticArgument.TextArgument("relationship", "INHERITANCE")) + .argument(new DiagnosticArgument.IdentifierListArgument("cycle", + baseChain(decl.id()).stream().map(SymbolId::value).toList()))); + } + + private List localMembers(DeclInfo decl) { + List result = new ArrayList<>(); + int position = 0; + for (DeclInfo child : index.children(decl.id())) { + work.checkpoint(); + if (child.kind() == SymbolKind.ATTRIBUTE) { + result.add(new EffectiveMember(child.id(), decl.id(), List.of(decl.id()), position++, + EffectiveMemberOrigin.LOCAL, false)); + } + } + return List.copyOf(result); + } + + private List pathTo(List chain, int depth) { + List result = new ArrayList<>(); + for (int i = depth; i >= 0; i--) { + work.checkpoint(); + result.add(chain.get(i)); + } + return List.copyOf(result); + } + + void bindBase(SymbolId symbol, SymbolId base) { state.bindBase(symbol, base); } + + void setEffectiveMembers(SymbolId owner, List members) { state.setEffectiveMembers(owner, members); } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/LineForms.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/LineForms.java index 0002b0e5..01559289 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/LineForms.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/LineForms.java @@ -4,55 +4,94 @@ import guru.interlis.model.*; import guru.interlis.syntax.LineFormDeclarationSyntax; import guru.interlis.syntax.LineTypeSyntax; + import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; /** Bound line declarations and predefined segment types; state belongs to the compilation graph. */ final class LineForms { - private final GraphBuilder g; - LineForms(GraphBuilder graph) { g = graph; } + private final SemanticGraphRead.Symbols state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameBindings names; + private final InheritanceBindings inheritance; + private final ReferenceBindings references; + private final Supplier typeBindings; + private final UnitBindings units; + private final Supplier expressions; + private final Function symbolHeader; + private final Function ensureSymbol; + + LineForms(SemanticGraphRead.Symbols state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameBindings names, + InheritanceBindings inheritance, + ReferenceBindings references, + Supplier typeBindings, + UnitBindings units, + Supplier expressions, + Function symbolHeader, + Function ensureSymbol) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.names = names; + this.inheritance = inheritance; + this.references = references; + this.typeBindings = typeBindings; + this.units = units; + this.expressions = expressions; + this.symbolHeader = symbolHeader; + this.ensureSymbol = ensureSymbol; + } LineFormSymbol buildForm(DeclInfo decl) { var syntax = (LineFormDeclarationSyntax) decl.syntax(); List errors = new ArrayList<>(); - var target = g.bindTarget(decl, syntax.segmentStructure(), Set.of(SymbolKind.STRUCTURE), ReferenceUsage.TYPE_USE, errors); + var target = names.bindTarget(decl, syntax.segmentStructure(), Set.of(SymbolKind.STRUCTURE), ReferenceUsage.TYPE_USE, errors); target.ifPresent(id -> { SymbolId base = id; Set seen = new HashSet<>(); - while (base != null && seen.add(base)) { g.checkWork(); base = g.objectInheritanceBase(base).orElse(null); } + while (base != null && seen.add(base)) { work.checkpoint(); base = inheritance.objectInheritanceBase(base).orElse(null); } SymbolId required = predefined(decl, "INTERLIS.LineSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow(); - if (id.equals(required) || !guru.interlis.model.internal.TypeCompatibility.specializes(id, required, g.baseOf::get)) - g.emitRule(decl, "line form structure must extend INTERLIS.LineSegment", "LINE_FORM_STRUCTURE"); + if (id.equals(required) || !guru.interlis.model.internal.TypeCompatibility.specializes(id, required, state::baseOf)) + diagnostics.emitRule(decl, "line form structure must extend INTERLIS.LineSegment", "LINE_FORM_STRUCTURE"); }); - var model = g.index.models().stream().filter(e -> g.index.modelOfSymbol(decl.id()).filter(e.id()::equals).isPresent()) + var model = index.models().stream().filter(e -> index.modelOfSymbol(decl.id()).filter(e.id()::equals).isPresent()) .findFirst().orElseThrow().model().syntax(); if (decl.language() == LanguageVersion.ILI23 && !model.contracted()) - g.emitRule(decl, "custom line forms require CONTRACTED in INTERLIS 2.3", "LINE_FORM_CONTRACTED"); - if (g.index.children(decl.owner().orElseThrow()).stream().anyMatch(other -> !other.id().equals(decl.id()) + diagnostics.emitRule(decl, "custom line forms require CONTRACTED in INTERLIS 2.3", "LINE_FORM_CONTRACTED"); + if (index.children(decl.owner().orElseThrow()).stream().anyMatch(other -> !other.id().equals(decl.id()) && other.name().equals(decl.name()) && other.kind() != SymbolKind.PARAMETER)) - g.emitRule(decl, "line form name is not unique in its scope", "LINE_FORM_NAME"); - return new LineFormSymbol(g.header(decl), target); + diagnostics.emitRule(decl, "line form name is not unique in its scope", "LINE_FORM_NAME"); + return new LineFormSymbol(symbolHeader.apply(decl), target); } GeometryType buildType(TypeId id, LineTypeSyntax syntax, DeclInfo decl) { List errors = new ArrayList<>(); List forms = new ArrayList<>(); for (var name : syntax.lineForms()) { - g.checkWork(); + work.checkpoint(); var target = predefined(decl, name.text(), Set.of(SymbolKind.LINE_FORM)); - if (target.isPresent()) g.recordReference(decl, name, target.orElseThrow(), ReferenceUsage.TYPE_USE); - else target = g.bindTarget(decl, name, Set.of(SymbolKind.LINE_FORM), ReferenceUsage.TYPE_USE, errors); + if (target.isPresent()) references.recordReference(decl, name, target.orElseThrow(), ReferenceUsage.TYPE_USE); + else target = names.bindTarget(decl, name, Set.of(SymbolKind.LINE_FORM), ReferenceUsage.TYPE_USE, errors); target.ifPresent(forms::add); } - var vertex = syntax.vertexDomain().flatMap(name -> g.bindTarget(decl, name, Set.of(SymbolKind.DOMAIN), ReferenceUsage.TYPE_USE, errors)); + var vertex = syntax.vertexDomain().flatMap(name -> names.bindTarget(decl, name, Set.of(SymbolKind.DOMAIN), ReferenceUsage.TYPE_USE, errors)); vertex.ifPresent(target -> { - g.index.byId(target).ifPresent(g::buildSymbol); - var domain = (DomainSymbol)g.symbols.get(target); - if (!(g.effectiveDescriptor(domain.typeId()) instanceof CoordinateType coordinate) || coordinate.multiCoordinate()) - g.emitRule(decl, "VERTEX must reference a coordinate domain", "LINE_VERTEX_COORDINATE"); + index.byId(target).ifPresent(ensureSymbol::apply); + var domain = (DomainSymbol)state.symbol(target); + if (!(typeBindings.get().effectiveDescriptor(domain.typeId()) instanceof CoordinateType coordinate) || coordinate.multiCoordinate()) + diagnostics.emitRule(decl, "VERTEX must reference a coordinate domain", "LINE_VERTEX_COORDINATE"); }); if (decl.language() == LanguageVersion.ILI24 && syntax.lineAttributes().isPresent()) - g.emitRule(decl, "LINE ATTRIBUTES are not supported by INTERLIS 2.4", "LINE_ATTRIBUTES_VERSION"); - var attributes = syntax.lineAttributes().flatMap(name -> g.bindTarget(decl, name, Set.of(SymbolKind.STRUCTURE), ReferenceUsage.TYPE_USE, errors)); + diagnostics.emitRule(decl, "LINE ATTRIBUTES are not supported by INTERLIS 2.4", "LINE_ATTRIBUTES_VERSION"); + var attributes = syntax.lineAttributes().flatMap(name -> names.bindTarget(decl, name, Set.of(SymbolKind.STRUCTURE), ReferenceUsage.TYPE_USE, errors)); Optional overlap = syntax.overlap().map(DecimalLiteral::parse).map(GeometryType.Explicit::new); return new GeometryType(id, new Origin.DeclaredOrigin(syntax.range()), syntax.geometryKind(), forms, vertex, attributes, syntax.directed(), syntax.withoutOverlaps(), overlap); @@ -62,60 +101,60 @@ GeometryType buildType(TypeId id, LineTypeSyntax syntax, DeclInfo decl) { * Aliases are read, never mutated; direction and LINE ATTRIBUTES in an explicit * line declaration retain their language meaning rather than being invented. */ void effective(TypeId id, TypeDescriptor inherited, DeclInfo declaration) { - if (!(g.types.get(id) instanceof GeometryType own)) return; + if (!(state.type(id) instanceof GeometryType own)) return; GeometryType parent = inherited instanceof GeometryType p ? p : null; - if (inherited != null && parent == null) g.emitRule(declaration, "geometry needs a geometric base", "GEOMETRY_EXTENSION"); + if (inherited != null && parent == null) diagnostics.emitRule(declaration, "geometry needs a geometric base", "GEOMETRY_EXTENSION"); var forms = own.lineForms().isEmpty() && parent != null ? parent.lineForms() : own.lineForms(); var vertex = own.vertexDomain().isEmpty() && parent != null ? parent.vertexDomain() : own.vertexDomain(); var tolerance = own.overlap().isEmpty() && parent != null ? parent.overlap() : own.overlap(); boolean surface = switch (own.geometryKind()) { case SURFACE, AREA, MULTISURFACE, MULTIAREA -> true; default -> false; }; boolean without = own.withoutOverlaps() || parent != null && parent.withoutOverlaps() || surface; if (tolerance.isEmpty() && without && declaration.language() == LanguageVersion.ILI24 && vertex.isPresent() - && g.effectiveDescriptor(g.declaredTypes.get(vertex.orElseThrow())) instanceof CoordinateType c + && typeBindings.get().effectiveDescriptor(state.declaredType(vertex.orElseThrow())) instanceof CoordinateType c && c.axes().stream().allMatch(a -> a.minimum().isPresent() && a.maximum().isPresent())) tolerance = Optional.of(new GeometryType.TrackWidth(vertex.orElseThrow())); var result = new GeometryType(id, own.origin(), own.geometryKind(), forms, vertex, own.lineAttributes(), own.directed(), without, tolerance); - g.types.put(id, result); + typeBindings.get().replaceType(result); if (parent != null) { var relation = extension(result, parent); if (relation.decision() == RelationDecision.DISALLOWED) - g.emitRule(declaration, "geometry extension: " + relation.reason(), "GEOMETRY_EXTENSION"); + diagnostics.emitRule(declaration, "geometry extension: " + relation.reason(), "GEOMETRY_EXTENSION"); } if (!result.lineAttributes().isEmpty() && !surface) - g.emitRule(declaration, "LINE ATTRIBUTES require SURFACE or AREA", "LINE_ATTRIBUTES_FAMILY"); + diagnostics.emitRule(declaration, "LINE ATTRIBUTES require SURFACE or AREA", "LINE_ATTRIBUTES_FAMILY"); boolean generic = declaration.language() == LanguageVersion.ILI24 && vertex.filter(this::genericVertex).isPresent(); - CoordinateType coordinate = vertex.map(g.declaredTypes::get).map(g::effectiveDescriptor) + CoordinateType coordinate = vertex.map(state::declaredType).map(typeBindings.get()::effectiveDescriptor) .filter(CoordinateType.class::isInstance).map(CoordinateType.class::cast).orElse(null); if (coordinate != null && declaration.language() == LanguageVersion.ILI24) { - var length = g.builtinUnit(declaration.id(), "INTERLIS.LENGTH").orElseThrow(); - for (var axis : coordinate.axes()) if (axis.unit().isPresent() && !g.unitExtends(axis.unit().orElseThrow(), length)) - g.emitRule(declaration, "line vertices require Cartesian coordinates with length units", "LINE_VERTEX_CARTESIAN"); + var length = units.builtinUnit(declaration.id(), "INTERLIS.LENGTH").orElseThrow(); + for (var axis : coordinate.axes()) if (axis.unit().isPresent() && !units.unitExtends(axis.unit().orElseThrow(), length)) + diagnostics.emitRule(declaration, "line vertices require Cartesian coordinates with length units", "LINE_VERTEX_CARTESIAN"); Integer scale = null; for (var axis : coordinate.axes()) { - g.checkWork(); + work.checkpoint(); if (axis.minimum().isEmpty() || axis.maximum().isEmpty()) continue; int value = scale(axis.minimum().orElseThrow()); if (value != scale(axis.maximum().orElseThrow()) || scale != null && scale != value) - g.emitRule(declaration, "line vertex axes must have the same numerical precision", "LINE_VERTEX_PRECISION"); + diagnostics.emitRule(declaration, "line vertex axes must have the same numerical precision", "LINE_VERTEX_PRECISION"); scale = value; } } if (declaration.language() == LanguageVersion.ILI23 && own.withoutOverlaps() && own.overlap().isEmpty()) - g.emitRule(declaration, "INTERLIS 2.3 WITHOUT OVERLAPS requires an explicit value", "OVERLAP_VERSION"); + diagnostics.emitRule(declaration, "INTERLIS 2.3 WITHOUT OVERLAPS requires an explicit value", "OVERLAP_VERSION"); if (own.overlap().orElse(null) instanceof GeometryType.Explicit explicit) { if (explicit.value().exactValue().signum() <= 0) - g.emitRule(declaration, "overlap tolerance must be greater than zero", "OVERLAP_POSITIVE"); + diagnostics.emitRule(declaration, "overlap tolerance must be greater than zero", "OVERLAP_POSITIVE"); if (declaration.language() == LanguageVersion.ILI24 && coordinate != null && coordinate.axes().getFirst().minimum().isPresent() && scale(explicit.value()) != scale(coordinate.axes().getFirst().minimum().orElseThrow())) - g.emitRule(declaration, "overlap tolerance must have the vertex precision", "OVERLAP_PRECISION"); + diagnostics.emitRule(declaration, "overlap tolerance must have the vertex precision", "OVERLAP_PRECISION"); } if (incomplete(result) && !generic && !abstractContext(declaration)) - g.emitRule(declaration, "concrete geometry requires line forms, concrete vertices and the versioned tolerance", "GEOMETRY_INCOMPLETE"); + diagnostics.emitRule(declaration, "concrete geometry requires line forms, concrete vertices and the versioned tolerance", "GEOMETRY_INCOMPLETE"); } TypeRelation extension(GeometryType derived, GeometryType base) { - return guru.interlis.model.internal.TypeCompatibility.geometryExtension(derived, base, g.baseOf::get, - id -> g.effectiveDescriptor(g.declaredTypes.get(id))); + return guru.interlis.model.internal.TypeCompatibility.geometryExtension(derived, base, state::baseOf, + id -> typeBindings.get().effectiveDescriptor(state.declaredType(id))); } private static int scale(DecimalLiteral value) { @@ -125,39 +164,39 @@ private static int scale(DecimalLiteral value) { private boolean genericVertex(SymbolId domain) { var seen = new HashSet(); var pending = new ArrayDeque(); pending.add(domain); while (!pending.isEmpty()) { - g.checkWork(); var id = pending.removeFirst(); if (!seen.add(id)) continue; - if (g.symbols.get(id) instanceof DomainSymbol d && d.generic()) return true; - if (g.types.get(g.declaredTypes.get(id)) instanceof DomainReferenceType alias) pending.add(alias.targetDomain()); - if (g.baseOf.get(id) != null) pending.add(g.baseOf.get(id)); + work.checkpoint(); var id = pending.removeFirst(); if (!seen.add(id)) continue; + if (state.symbol(id) instanceof DomainSymbol d && d.generic()) return true; + if (state.type(state.declaredType(id)) instanceof DomainReferenceType alias) pending.add(alias.targetDomain()); + if (state.baseOf(id) != null) pending.add(state.baseOf(id)); } return false; } private boolean abstractContext(DeclInfo declaration) { if (declaration.modifiers().contains(Modifier.ABSTRACT)) return true; - return declaration.kind() == SymbolKind.ATTRIBUTE && declaration.owner().flatMap(g.index::byId) + return declaration.kind() == SymbolKind.ATTRIBUTE && declaration.owner().flatMap(index::byId) .filter(d -> d.modifiers().contains(Modifier.ABSTRACT)).isPresent(); } private boolean incomplete(GeometryType geometry) { if (geometry.lineForms().isEmpty() || geometry.vertexDomain().isEmpty()) return true; - var descriptor = g.effectiveDescriptor(g.declaredTypes.get(geometry.vertexDomain().orElseThrow())); - if (g.symbols.get(geometry.vertexDomain().orElseThrow()).header().has(Modifier.ABSTRACT)) return true; + var descriptor = typeBindings.get().effectiveDescriptor(state.declaredType(geometry.vertexDomain().orElseThrow())); + if (state.symbol(geometry.vertexDomain().orElseThrow()).header().has(Modifier.ABSTRACT)) return true; if (!(descriptor instanceof CoordinateType c) || c.axes().stream().anyMatch(a -> a.minimum().isEmpty() || a.maximum().isEmpty())) return true; return geometry.withoutOverlaps() && geometry.overlap().isEmpty(); } void validateConcreteMembers() { - for (var entry : g.effectiveMembers.entrySet()) { - g.checkWork(); var owner = g.index.byId(entry.getKey()).orElse(null); + for (var entry : state.effectiveMembersInBuildOrder()) { + work.checkpoint(); var owner = index.byId(entry.getKey()).orElse(null); if (owner == null || owner.modifiers().contains(Modifier.ABSTRACT)) continue; for (var member : entry.getValue()) { - var symbol = g.symbols.get(member.declaredSymbol()); + var symbol = state.symbol(member.declaredSymbol()); if (!(symbol instanceof AttributeSymbol attribute)) continue; - var type = g.effectiveDescriptor(g.declaredTypes.get(attribute.id())); + var type = typeBindings.get().effectiveDescriptor(state.declaredType(attribute.id())); if (type instanceof GeometryType geometry && (attribute.header().has(Modifier.ABSTRACT) || incomplete(geometry) && !(owner.language() == LanguageVersion.ILI24 && geometry.vertexDomain().filter(this::genericVertex).isPresent()))) - g.emitRule(owner, "concrete owner retains an abstract geometry member", "GEOMETRY_ABSTRACT_MEMBER"); + diagnostics.emitRule(owner, "concrete owner retains an abstract geometry member", "GEOMETRY_ABSTRACT_MEMBER"); } } } @@ -173,11 +212,11 @@ Optional predefined(DeclInfo context, String name, Set kin "INTERLIS.SurfaceBoundary", "INTERLIS.LineGeometry").contains(name))) return Optional.empty(); LanguageVersion language = context.language(); SymbolId id = id(language, form ? name : name.substring(9)); - if (g.symbols.containsKey(id)) return Optional.of(id); + if (state.hasSymbol(id)) return Optional.of(id); if (form) { SymbolId structure = predefined(context, name.equals("STRAIGHTS") ? "INTERLIS.StraightSegment" : "INTERLIS.ArcSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow(); - g.symbols.put(id, new LineFormSymbol(header(language, name, SymbolKind.LINE_FORM, Optional.empty(), Set.of()), Optional.of(structure))); + state.defineSymbol(new LineFormSymbol(header(language, name, SymbolKind.LINE_FORM, Optional.empty(), Set.of()), Optional.of(structure))); return Optional.of(id); } String shortName = name.substring(9); @@ -185,53 +224,53 @@ Optional predefined(DeclInfo context, String name, Set kin return Optional.of(geometryStructure(context, shortName)); Optional base = shortName.equals("LineSegment") ? Optional.empty() : predefined(context, "INTERLIS.LineSegment", Set.of(SymbolKind.STRUCTURE)); - base.ifPresent(parent -> g.baseOf.put(id, parent)); + base.ifPresent(parent -> inheritance.bindBase(id, parent)); var header = header(language, shortName, SymbolKind.STRUCTURE, Optional.empty(), Set.of(shortName.equals("LineSegment") ? Modifier.ABSTRACT : Modifier.FINAL)); List members = new ArrayList<>(); if (shortName.equals("LineSegment")) members.add(attribute(language, id, "SegmentEndPoint", lineCoord(language), true)); if (shortName.equals("ArcSegment")) { members.add(attribute(language, id, "ArcPoint", lineCoord(language), true)); - TypeId numeric = g.nextTypeId(); - var unit = g.builtinUnit(context.id(), "INTERLIS.LENGTH"); - g.types.put(numeric, new NumericType(numeric, header.origin(), new NumericRange(Optional.empty(), Optional.empty(), false, + TypeId numeric = typeBindings.get().nextTypeId(); + var unit = units.builtinUnit(context.id(), "INTERLIS.LENGTH"); + typeBindings.get().defineType(new NumericType(numeric, header.origin(), new NumericRange(Optional.empty(), Optional.empty(), false, NumericDirection.NONE, unit, Optional.empty()))); members.add(attribute(language, id, "Radius", numeric, false)); } - g.symbols.put(id, new StructureSymbol(header, base, members, List.of())); + state.defineSymbol(new StructureSymbol(header, base, members, List.of())); List effective = new ArrayList<>(); - base.ifPresent(parent -> g.effectiveMembers.getOrDefault(parent, List.of()).forEach(member -> effective.add( + base.ifPresent(parent -> state.effectiveMembers(parent).forEach(member -> effective.add( new EffectiveMember(member.declaredSymbol(), id, List.of(parent, id), effective.size(), EffectiveMemberOrigin.INHERITED, false)))); for (SymbolId member : members) effective.add(new EffectiveMember(member, id, List.of(id), effective.size(), EffectiveMemberOrigin.LOCAL, false)); - g.effectiveMembers.put(id, List.copyOf(effective)); + inheritance.setEffectiveMembers(id, List.copyOf(effective)); return Optional.of(id); } /** Appendix-A declarations retain unspecified geometry rather than borrowing a source vertex. */ private SymbolId geometryStructure(DeclInfo context, String name) { - g.checkWork(); + work.checkpoint(); var language = context.language(); var h = header(language, name, SymbolKind.STRUCTURE, Optional.empty(), Set.of()); List members = new ArrayList<>(); if (name.equals("SurfaceEdge")) { - TypeId geometry = g.nextTypeId(); - g.types.put(geometry, new GeometryType(geometry, h.origin(), GeometryKind.POLYLINE, List.of(), + TypeId geometry = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new GeometryType(geometry, h.origin(), GeometryKind.POLYLINE, List.of(), Optional.empty(), Optional.empty(), true, false, Optional.empty())); members.add(attribute(language, h.id(), "Geometry", geometry, false)); if (language == LanguageVersion.ILI23) { - TypeId attrs = g.nextTypeId(); g.types.put(attrs, new GenericType(attrs, h.origin(), "ANYSTRUCTURE")); + TypeId attrs = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new GenericType(attrs, h.origin(), "ANYSTRUCTURE")); members.add(attribute(language, h.id(), "LineAttrs", attrs, false, 1)); } } else { String targetName = name.equals("SurfaceBoundary") ? "SurfaceEdge" : "LineSegment"; var target = predefined(context, "INTERLIS." + targetName, Set.of(SymbolKind.STRUCTURE)).orElseThrow(); - TypeId element = g.nextTypeId(); g.types.put(element, new StructureValueType(element, h.origin(), Optional.of(target), List.of())); - TypeId list = g.nextTypeId(); g.types.put(list, new CollectionType(list, h.origin(), CollectionKind.LIST, Cardinality.unboundedFrom(0), element)); + TypeId element = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new StructureValueType(element, h.origin(), Optional.of(target), List.of())); + TypeId list = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new CollectionType(list, h.origin(), CollectionKind.LIST, Cardinality.unboundedFrom(0), element)); members.add(attribute(language, h.id(), name.equals("SurfaceBoundary") ? "Lines" : "Segments", list, false)); } - g.symbols.put(h.id(), new StructureSymbol(h, Optional.empty(), members, List.of())); + state.defineSymbol(new StructureSymbol(h, Optional.empty(), members, List.of())); List effective = new ArrayList<>(); for (SymbolId member : members) effective.add(new EffectiveMember(member, h.id(), List.of(h.id()), effective.size(), EffectiveMemberOrigin.LOCAL, false)); - g.effectiveMembers.put(h.id(), List.copyOf(effective)); + inheritance.setEffectiveMembers(h.id(), List.copyOf(effective)); if (name.equals("LineGeometry")) { var segment = predefined(context, "INTERLIS.LineSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow(); var start = predefined(context, "INTERLIS.StartSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow(); @@ -243,36 +282,36 @@ private SymbolId geometryStructure(DeclInfo context, String name) { } void startConstraint(DeclInfo owner, SymbolId line, SymbolId segment, SymbolId start, guru.interlis.source.SourceRange range) { - var structure = (StructureSymbol)g.symbols.get(line); + var structure = (StructureSymbol)state.symbol(line); if (!structure.constraints().isEmpty()) return; SymbolId attribute = structure.members().getFirst(); - TypeId element = g.nextTypeId(); g.types.put(element, new StructureValueType(element, structure.origin(), Optional.of(segment), List.of())); + TypeId element = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new StructureValueType(element, structure.origin(), Optional.of(segment), List.of())); ExpressionId path = new ExpressionId(line.value() + ":first"); - g.expressions.put(path, new PathExpression(path, element, ExpressionTypeState.RESOLVED, range, List.of(), new ObjectPath(List.of( + expressions.get().defineExpression(new PathExpression(path, element, ExpressionTypeState.RESOLVED, range, List.of(), new ObjectPath(List.of( new PathStep.AttributeStep("Segments", attribute, range), new PathStep.IndexStep(new PathIndex(PathIndex.Kind.FIRST, Optional.empty()), element, range))))); - TypeId classType = g.nextTypeId(); g.types.put(classType, new ClassValueType(classType, structure.origin(), true, List.of(start))); + TypeId classType = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new ClassValueType(classType, structure.origin(), true, List.of(start))); ExpressionId value = new ExpressionId(line.value() + ":start"); - g.expressions.put(value, new SymbolValueExpression(value, classType, ExpressionTypeState.RESOLVED, range, start)); - TypeId bool = g.nextTypeId(); g.types.put(bool, new BooleanType(bool, structure.origin())); + expressions.get().defineExpression(new SymbolValueExpression(value, classType, ExpressionTypeState.RESOLVED, range, start)); + TypeId bool = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new BooleanType(bool, structure.origin())); ExpressionId call = new ExpressionId(line.value() + ":isStart"); - g.expressions.put(call, new FunctionCallExpression(call, bool, ExpressionTypeState.RESOLVED, range, List.of(path, value), "INTERLIS.isOfClass", Optional.of(g.builtinDeclaration(BuiltinFunctions.find("isOfClass").orElseThrow(),owner.language()).id()), Map.of())); + expressions.get().defineExpression(new FunctionCallExpression(call, bool, ExpressionTypeState.RESOLVED, range, List.of(path, value), "INTERLIS.isOfClass", Optional.of(expressions.get().builtinDeclaration(BuiltinFunctions.find("isOfClass").orElseThrow(),owner.language()).id()), Map.of())); SymbolId constraint = new SymbolId(line.value() + ":startConstraint"); Origin constraintOrigin = structure.origin() instanceof Origin.PredefinedOrigin predefined ? new Origin.PredefinedOrigin(predefined.language(), predefined.key() + ".StartSegment") - : new Origin.DerivedOrigin(((StructureSymbol)g.symbols.get(((Origin.DerivedOrigin)structure.origin()).declaredAt())).constraints().getFirst(), DerivationKind.CONTEXTUALIZATION); + : new Origin.DerivedOrigin(((StructureSymbol)state.symbol(((Origin.DerivedOrigin)structure.origin()).declaredAt())).constraints().getFirst(), DerivationKind.CONTEXTUALIZATION); var header = new SymbolHeader(constraint, SymbolKind.CONSTRAINT, DeclaredName.synthetic("StartSegment"), Optional.of(line), 0, constraintOrigin, Optional.empty(), owner.language(), Documentation.empty(), MetaAttributeSet.empty(), Set.of(), List.of()); - g.symbols.put(constraint, new ConstraintSymbol(header, new MandatoryConstraint(Optional.empty(), call), Optional.empty())); - g.symbols.put(line, new StructureSymbol(structure.header(), structure.baseType(), structure.members(), List.of(constraint))); + state.defineSymbol(new ConstraintSymbol(header, new MandatoryConstraint(Optional.empty(), call), Optional.empty())); + state.replaceSymbol(new StructureSymbol(structure.header(), structure.baseType(), structure.members(), List.of(constraint))); } private TypeId lineCoord(LanguageVersion language) { SymbolId domain = id(language, "LineCoord"); - if (g.declaredTypes.containsKey(domain)) return g.declaredTypes.get(domain); - TypeId type = g.nextTypeId(); + if (state.hasDeclaredType(domain)) return state.declaredType(domain); + TypeId type = typeBindings.get().nextTypeId(); var h = header(language, "LineCoord", SymbolKind.DOMAIN, Optional.empty(), Set.of(Modifier.ABSTRACT)); - g.types.put(type, new CoordinateType(type, h.origin(), 2, List.of(NumericRange.unbounded(), NumericRange.unbounded()), false, Optional.empty(), Optional.empty())); - g.symbols.put(domain, new DomainSymbol(h, type, Optional.empty(), false)); g.declaredTypes.put(domain, type); + typeBindings.get().defineType(new CoordinateType(type, h.origin(), 2, List.of(NumericRange.unbounded(), NumericRange.unbounded()), false, Optional.empty(), Optional.empty())); + state.defineSymbol(new DomainSymbol(h, type, Optional.empty(), false)); typeBindings.get().bindDeclaredType(domain, type); return type; } @@ -285,8 +324,8 @@ private SymbolId attribute(LanguageVersion language, SymbolId owner, String name // Attribute names are local even though their predefined identity includes the owning structure. h = new SymbolHeader(h.id(), h.kind(), DeclaredName.declared(name), h.owner(), order, h.origin(), h.nameRange(), h.languageVersion(), h.documentation(), h.metaAttributes(), h.modifiers(), h.oidRules()); - g.symbols.put(h.id(), new AttributeSymbol(h, type, mandatory, false, false, false, Optional.empty(), List.of())); - g.declaredTypes.put(h.id(), type); return h.id(); + state.defineSymbol(new AttributeSymbol(h, type, mandatory, false, false, false, Optional.empty(), List.of())); + typeBindings.get().bindDeclaredType(h.id(), type); return h.id(); } private SymbolId id(LanguageVersion language, String name) { return new SymbolId("predefined:" + language + ":" + name); } diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/MetadataBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/MetadataBindings.java index 43fd5b89..fc459a75 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/MetadataBindings.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/MetadataBindings.java @@ -1,33 +1,81 @@ package guru.interlis.compiler.internal; -import guru.interlis.model.*; import guru.interlis.diagnostics.DiagnosticId; +import guru.interlis.model.*; import guru.interlis.syntax.*; + import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; /** Metadata binding within one graph build. External object values are never loaded. */ final class MetadataBindings { - private final GraphBuilder g; + private final SemanticGraphRead.Symbols state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameResolver resolver; + private final NameBindings names; + private final ReferenceBindings references; + private final InheritanceBindings inheritance; + private final UnitBindings units; + private final Supplier typeBindings; + private final ObjectPathBindings paths; + private final StructureBindings structures; + private final Supplier expressions; + private final Function symbolHeader; + private final Function ensureSymbol; private final Set active = new HashSet<>(); - MetadataBindings(GraphBuilder graph) { g = graph; } + + MetadataBindings(SemanticGraphRead.Symbols state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameResolver resolver, + NameBindings names, + ReferenceBindings references, + InheritanceBindings inheritance, + UnitBindings units, + Supplier typeBindings, + ObjectPathBindings paths, + StructureBindings structures, + Supplier expressions, + Function symbolHeader, + Function ensureSymbol) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.resolver = resolver; + this.names = names; + this.references = references; + this.inheritance = inheritance; + this.units = units; + this.typeBindings = typeBindings; + this.paths = paths; + this.structures = structures; + this.expressions = expressions; + this.symbolHeader = symbolHeader; + this.ensureSymbol = ensureSymbol; + } MetadataBasketSymbol basket(DeclInfo decl) { var syntax = (MetadataBasketDeclarationSyntax)decl.syntax(); - var topic = g.bindTarget(decl, syntax.topic(), Set.of(SymbolKind.TOPIC), ReferenceUsage.TYPE_USE, new ArrayList<>()); - var base = syntax.baseBasket().flatMap(name -> g.bindTarget(decl, name, Set.of(SymbolKind.METADATA_BASKET), ReferenceUsage.BASE_TYPE, new ArrayList<>())); - var result = new MetadataBasketSymbol(g.header(decl), syntax.basketKind(), topic, base, - g.index.children(decl.id()).stream().map(DeclInfo::id).toList()); - g.symbols.put(decl.id(), result); + var topic = names.bindTarget(decl, syntax.topic(), Set.of(SymbolKind.TOPIC), ReferenceUsage.TYPE_USE, new ArrayList<>()); + var base = syntax.baseBasket().flatMap(name -> names.bindTarget(decl, name, Set.of(SymbolKind.METADATA_BASKET), ReferenceUsage.BASE_TYPE, new ArrayList<>())); + var result = new MetadataBasketSymbol(symbolHeader.apply(decl), syntax.basketKind(), topic, base, + index.children(decl.id()).stream().map(DeclInfo::id).toList()); + state.defineSymbol(result); active.add(decl.id()); base.ifPresent(id -> { - if (active.contains(id)) { g.emitRule(decl, "cyclic metadata basket inheritance", "METADATA_BASKET_CYCLE"); return; } - var parent = (MetadataBasketSymbol)g.buildSymbol(g.index.byId(id).orElseThrow()); - if (parent.header().has(Modifier.FINAL)) g.emitRule(decl, "cannot extend FINAL metadata basket", "METADATA_BASKET_FINAL"); - if (parent.basketKind() != syntax.basketKind()) g.emitRule(decl, "metadata basket kind differs from base", "METADATA_BASKET_KIND"); + if (active.contains(id)) { diagnostics.emitRule(decl, "cyclic metadata basket inheritance", "METADATA_BASKET_CYCLE"); return; } + var parent = (MetadataBasketSymbol)ensureSymbol.apply(index.byId(id).orElseThrow()); + if (parent.header().has(Modifier.FINAL)) diagnostics.emitRule(decl, "cannot extend FINAL metadata basket", "METADATA_BASKET_FINAL"); + if (parent.basketKind() != syntax.basketKind()) diagnostics.emitRule(decl, "metadata basket kind differs from base", "METADATA_BASKET_KIND"); if (topic.isPresent() && parent.topic().isPresent() && !topicSpecializes(topic.get(), parent.topic().get())) - g.emitRule(decl, "metadata basket topic must equal or extend its base topic", "METADATA_BASKET_TOPIC"); + diagnostics.emitRule(decl, "metadata basket topic must equal or extend its base topic", "METADATA_BASKET_TOPIC"); }); - for (var child : g.index.children(decl.id())) { g.checkWork(); g.buildSymbol(child); } + for (var child : index.children(decl.id())) { work.checkpoint(); ensureSymbol.apply(child); } active.remove(decl.id()); return result; } @@ -35,54 +83,54 @@ MetadataBasketSymbol basket(DeclInfo decl) { private boolean topicSpecializes(SymbolId actual, SymbolId expected) { Set seen = new HashSet<>(); while (actual != null && seen.add(actual)) { - g.checkWork(); if (actual.equals(expected)) return true; - var declaration = g.index.byId(actual).orElseThrow(); + work.checkpoint(); if (actual.equals(expected)) return true; + var declaration = index.byId(actual).orElseThrow(); actual = ((TopicSyntax)declaration.syntax()).baseTopic().flatMap(name -> - g.resolver.resolve(declaration.id(), name, Set.of(SymbolKind.TOPIC)).target()).orElse(null); + resolver.resolve(declaration.id(), name, Set.of(SymbolKind.TOPIC)).target()).orElse(null); } return false; } MetaObjectSymbol object(DeclInfo decl) { var syntax = (MetadataBasketDeclarationSyntax.ObjectDeclaration)decl.syntax(); - var owner = g.index.byId(decl.owner().orElseThrow()).orElseThrow(); - var basket = g.symbols.get(owner.id()) instanceof MetadataBasketSymbol b ? b : basket(owner); + var owner = index.byId(decl.owner().orElseThrow()).orElseThrow(); + var basket = state.symbol(owner.id()) instanceof MetadataBasketSymbol b ? b : basket(owner); Optional target = Optional.empty(); SymbolId topic = basket.topic().orElse(null); Set seen = new HashSet<>(); while (topic != null && seen.add(topic) && target.isEmpty()) { - g.checkWork(); - target = g.index.children(topic).stream().filter(d -> d.kind() == SymbolKind.CLASS && d.name().equals(syntax.objectClass().text())).map(DeclInfo::id).findFirst(); - var topicDecl = g.index.byId(topic).orElseThrow(); - topic = ((TopicSyntax)topicDecl.syntax()).baseTopic().flatMap(name -> g.resolver.resolve(topicDecl.id(), name, Set.of(SymbolKind.TOPIC)).target()).orElse(null); + work.checkpoint(); + target = index.children(topic).stream().filter(d -> d.kind() == SymbolKind.CLASS && d.name().equals(syntax.objectClass().text())).map(DeclInfo::id).findFirst(); + var topicDecl = index.byId(topic).orElseThrow(); + topic = ((TopicSyntax)topicDecl.syntax()).baseTopic().flatMap(name -> resolver.resolve(topicDecl.id(), name, Set.of(SymbolKind.TOPIC)).target()).orElse(null); } - g.recordReference(decl, syntax.objectClass(), target.orElse(null), ReferenceUsage.TYPE_USE); - if (target.isEmpty()) g.emitRule(decl, "metadata object class is not declared in the basket topic", "METAOBJECT_CLASS"); + references.recordReference(decl, syntax.objectClass(), target.orElse(null), ReferenceUsage.TYPE_USE); + if (target.isEmpty()) diagnostics.emitRule(decl, "metadata object class is not declared in the basket topic", "METAOBJECT_CLASS"); target.ifPresent(id -> { ensureBases(id); var root = predefined(decl, basket.basketKind() == MetadataBasketSymbol.Kind.REFSYSTEM ? "INTERLIS.REFSYSTEM" : "INTERLIS.SIGN", Set.of(SymbolKind.CLASS)).orElseThrow(); - if (!specializes(id, root)) g.emitRule(decl, "metadata object class has the wrong metadata family", "METAOBJECT_CLASS_KIND"); + if (!specializes(id, root)) diagnostics.emitRule(decl, "metadata object class has the wrong metadata family", "METAOBJECT_CLASS_KIND"); }); - if (g.index.children(owner.id()).stream().filter(d -> d.name().equals(decl.name())).count() > 1) - g.emitRule(decl, "duplicate metadata object name", "METAOBJECT_NAME"); - return new MetaObjectSymbol(g.header(decl), target); + if (index.children(owner.id()).stream().filter(d -> d.name().equals(decl.name())).count() > 1) + diagnostics.emitRule(decl, "duplicate metadata object name", "METAOBJECT_NAME"); + return new MetaObjectSymbol(symbolHeader.apply(decl), target); } void ensureBases(SymbolId id) { Set seen = new HashSet<>(); - while (id != null && seen.add(id)) { g.checkWork(); id = g.objectInheritanceBase(id).orElse(null); } + while (id != null && seen.add(id)) { work.checkpoint(); id = inheritance.objectInheritanceBase(id).orElse(null); } } private boolean specializes(SymbolId actual, SymbolId expected) { - return guru.interlis.model.internal.TypeCompatibility.specializes(actual, expected, g.baseOf::get); + return guru.interlis.model.internal.TypeCompatibility.specializes(actual, expected, state::baseOf); } NumericRange numeric(NumericTypeSyntax syntax, DeclInfo context, List errors) { - var unit = syntax.unit().flatMap(name -> g.bindUnit(context, name, errors)); + var unit = syntax.unit().flatMap(name -> units.bindUnit(context, name, errors)); var ref = syntax.referenceSystem().flatMap(r -> { if (r.axis().isPresent() && r.axis().getAsInt() < 1) { - errors.add(g.emitRule(context, "reference-system axis numbers start at one", "REFSYSTEM_AXIS")); return Optional.empty(); + errors.add(diagnostics.emitRule(context, "reference-system axis numbers start at one", "REFSYSTEM_AXIS")); return Optional.empty(); } - Optional target = r.domain() ? g.bindTarget(context, r.target(), Set.of(SymbolKind.DOMAIN), ReferenceUsage.TYPE_USE, errors) + Optional target = r.domain() ? names.bindTarget(context, r.target(), Set.of(SymbolKind.DOMAIN), ReferenceUsage.TYPE_USE, errors) : objectReference(context, r.target(),errors); return target.map(id -> new NumericRange.ReferenceSystem(id, r.domain(), r.axis())); }); @@ -94,52 +142,57 @@ Optional objectReference(DeclInfo context, NameSyntax name) { } private Optional objectReference(DeclInfo context, NameSyntax name,List errors) { - Optional target = g.resolver.resolve(context.id(), name, Set.of(SymbolKind.METAOBJECT)).target(); - if (target.isEmpty()) { + var resolution = resolver.resolve(context.id(), name, Set.of(SymbolKind.METAOBJECT)); + var target = resolution.target(); + if (resolution.status() == NameResolver.Status.UNRESOLVED) { List baskets = new ArrayList<>(); if (name.segments().size() > 1) { var prefix = new NameSyntax(name.id(), name.range(), name.segments().subList(0, name.segments().size() - 1)); - g.resolver.resolve(context.id(), prefix, Set.of(SymbolKind.METADATA_BASKET)).target().ifPresent(baskets::add); + resolver.resolve(context.id(), prefix, Set.of(SymbolKind.METADATA_BASKET)).target().ifPresent(baskets::add); } else { // A bare object name is legal only with one visible applicable basket. SymbolId scope = context.id(); while (scope != null && baskets.isEmpty()) { - for (var child : g.index.children(scope)) if (child.kind() == SymbolKind.METADATA_BASKET) baskets.add(child.id()); - scope = g.index.byId(scope).flatMap(DeclInfo::owner).orElse(null); + for (var child : index.children(scope)) if (child.kind() == SymbolKind.METADATA_BASKET) baskets.add(child.id()); + scope = index.byId(scope).flatMap(DeclInfo::owner).orElse(null); } } if (baskets.size() == 1) target = inheritedObject(baskets.getFirst(), name.segments().getLast().text(), new HashSet<>()); } - g.recordReference(context, name, target.orElse(null), ReferenceUsage.TYPE_USE); - if (target.isEmpty()) errors.add(g.emitRule(context, "metadata object reference is missing or ambiguous: " + name.text(), "METAOBJECT_REFERENCE")); - target.ifPresent(id -> g.buildSymbol(g.index.byId(id).orElseThrow())); + if (target.isPresent()) { + references.recordReference(context, name, target.orElseThrow(), ReferenceUsage.TYPE_USE); + } else { + references.recordResolution(context, name, resolution, ReferenceUsage.TYPE_USE); + errors.add(diagnostics.emitRule(context, "metadata object reference is missing or ambiguous: " + name.text(), "METAOBJECT_REFERENCE")); + } + target.ifPresent(id -> ensureSymbol.apply(index.byId(id).orElseThrow())); return target; } private Optional inheritedObject(SymbolId basketId, String name, Set seen) { if (!seen.add(basketId)) return Optional.empty(); - var basket = (MetadataBasketSymbol)g.buildSymbol(g.index.byId(basketId).orElseThrow()); - var local = g.index.children(basketId).stream().filter(d -> d.name().equals(name)).map(DeclInfo::id).toList(); + var basket = (MetadataBasketSymbol)ensureSymbol.apply(index.byId(basketId).orElseThrow()); + var local = index.children(basketId).stream().filter(d -> d.name().equals(name)).map(DeclInfo::id).toList(); if (!local.isEmpty()) return local.size() == 1 ? Optional.of(local.getFirst()) : Optional.empty(); return basket.baseBasket().flatMap(id -> inheritedObject(id, name, seen)); } void validate() { - for (var symbol : List.copyOf(g.symbols.values())) { - var decl = g.index.byId(symbol.id()).orElse(null); + for (var symbol : List.copyOf(state.symbolsInBuildOrder())) { + var decl = index.byId(symbol.id()).orElse(null); if (decl == null) continue; - if (symbol instanceof ParameterSymbol parameter && g.index.byId(decl.owner().orElseThrow()).orElseThrow().kind() != SymbolKind.MODEL) + if (symbol instanceof ParameterSymbol parameter && index.byId(decl.owner().orElseThrow()).orElseThrow().kind() != SymbolKind.MODEL) validateParameter(decl, parameter); } - for (var entry : List.copyOf(g.declaredTypes.entrySet())) { - var decl = g.index.byId(entry.getKey()).orElse(null); if (decl == null) continue; - var type = g.effectiveDescriptor(entry.getValue()); + for (var entry : List.copyOf(state.declaredTypesInBuildOrder())) { + var decl = index.byId(entry.getKey()).orElse(null); if (decl == null) continue; + var type = typeBindings.get().effectiveDescriptor(entry.getValue()); if (type instanceof NumericType n) validateRange(decl, n.range()); if (type instanceof CoordinateType c) { c.axes().forEach(axis -> validateRange(decl, axis)); c.rotation().ifPresent(r -> { if (r.zeroAxis() < 1 || r.quarterTurnAxis() < 1 || r.zeroAxis() > c.dimensions() || r.quarterTurnAxis() > c.dimensions() || r.zeroAxis() == r.quarterTurnAxis()) - g.emitRule(decl, "coordinate rotation needs two distinct existing axes", "COORDINATE_ROTATION"); }); + diagnostics.emitRule(decl, "coordinate rotation needs two distinct existing axes", "COORDINATE_ROTATION"); }); } } } @@ -150,44 +203,44 @@ private void validateParameter(DeclInfo decl, ParameterSymbol parameter) { var refsystem = predefined(decl, "INTERLIS.REFSYSTEM", Set.of(SymbolKind.CLASS)).orElseThrow(); var sign = predefined(decl, "INTERLIS.SIGN", Set.of(SymbolKind.CLASS)).orElseThrow(); if (specializes(owner, sign)) { - var inherited = parameter(g.baseOf.get(owner), decl.name()); - if (g.index.children(owner).stream().filter(d -> d.kind()==SymbolKind.PARAMETER && d.name().equals(decl.name())).count()>1) - g.emitRule(decl, "duplicate signature parameter", "PARAMETER_NAME_UNIQUE"); + var inherited = parameter(state.baseOf(owner), decl.name()); + if (index.children(owner).stream().filter(d -> d.kind()==SymbolKind.PARAMETER && d.name().equals(decl.name())).count()>1) + diagnostics.emitRule(decl, "duplicate signature parameter", "PARAMETER_NAME_UNIQUE"); if (inherited.isPresent() != decl.modifiers().contains(Modifier.EXTENDED)) - g.emitRule(decl, "signature parameter override needs an inherited parameter and EXTENDED", "PARAMETER_OVERRIDE"); + diagnostics.emitRule(decl, "signature parameter override needs an inherited parameter and EXTENDED", "PARAMETER_OVERRIDE"); inherited.ifPresent(id -> { - var parent=(ParameterSymbol)g.symbols.get(id); - if (parent.header().has(Modifier.FINAL)) g.emitRule(decl,"cannot override FINAL parameter","PARAMETER_FINAL"); - if (!g.assignable(parameter.typeId(),parent.typeId()) || parent.mandatory() && !parameter.mandatory()) - g.emitRule(decl,"signature parameter must refine inherited type and mandatory state","PARAMETER_TYPE"); + var parent=(ParameterSymbol)state.symbol(id); + if (parent.header().has(Modifier.FINAL)) diagnostics.emitRule(decl,"cannot override FINAL parameter","PARAMETER_FINAL"); + if (!typeBindings.get().assignable(parameter.typeId(),parent.typeId()) || parent.mandatory() && !parameter.mandatory()) + diagnostics.emitRule(decl,"signature parameter must refine inherited type and mandatory state","PARAMETER_TYPE"); }); - g.symbols.put(decl.id(),new ParameterSymbol(parameter.header(),parameter.typeId(),parameter.mandatory(),inherited)); + state.replaceSymbol(new ParameterSymbol(parameter.header(),parameter.typeId(),parameter.mandatory(),inherited)); return; } if (!(specializes(owner, axis) || specializes(owner, refsystem))) { - g.emitRule(decl, "class parameters require a reference-system, axis or signature class", "PARAMETER_OWNER"); return; + diagnostics.emitRule(decl, "class parameters require a reference-system, axis or signature class", "PARAMETER_OWNER"); return; } - var inherited = parameter(g.baseOf.get(owner), decl.name()); + var inherited = parameter(state.baseOf(owner), decl.name()); if (!decl.name().equals("Unit") || inherited.isEmpty()) { - g.emitRule(decl, "reference-system and axis parameters may only refine the predefined Unit", "PARAMETER_UNIT"); return; + diagnostics.emitRule(decl, "reference-system and axis parameters may only refine the predefined Unit", "PARAMETER_UNIT"); return; } - if (!decl.modifiers().contains(Modifier.EXTENDED)) g.emitRule(decl, "parameter override requires EXTENDED", "PARAMETER_OVERRIDE"); - var parent = (ParameterSymbol)g.symbols.get(inherited.get()); - var actual = g.effectiveDescriptor(parameter.typeId()); var expected = g.effectiveDescriptor(parent.typeId()); + if (!decl.modifiers().contains(Modifier.EXTENDED)) diagnostics.emitRule(decl, "parameter override requires EXTENDED", "PARAMETER_OVERRIDE"); + var parent = (ParameterSymbol)state.symbol(inherited.get()); + var actual = typeBindings.get().effectiveDescriptor(parameter.typeId()); var expected = typeBindings.get().effectiveDescriptor(parent.typeId()); if (!(actual instanceof NumericType a && expected instanceof NumericType b) - || b.range().unit().isPresent() && (a.range().unit().isEmpty() || !g.unitExtends(a.range().unit().get(), b.range().unit().get()))) - g.emitRule(decl, "Unit parameter must specialize the inherited numeric unit", "PARAMETER_UNIT_TYPE"); - if (parent.header().has(Modifier.FINAL)) g.emitRule(decl, "cannot override FINAL parameter", "PARAMETER_FINAL"); - g.symbols.put(decl.id(), new ParameterSymbol(parameter.header(), parameter.typeId(), parameter.mandatory(), inherited)); + || b.range().unit().isPresent() && (a.range().unit().isEmpty() || !units.unitExtends(a.range().unit().get(), b.range().unit().get()))) + diagnostics.emitRule(decl, "Unit parameter must specialize the inherited numeric unit", "PARAMETER_UNIT_TYPE"); + if (parent.header().has(Modifier.FINAL)) diagnostics.emitRule(decl, "cannot override FINAL parameter", "PARAMETER_FINAL"); + state.replaceSymbol(new ParameterSymbol(parameter.header(), parameter.typeId(), parameter.mandatory(), inherited)); } Optional parameter(SymbolId owner, String name) { Set seen = new HashSet<>(); while (owner != null && seen.add(owner)) { - g.checkWork(); var symbol = g.symbols.get(owner); + work.checkpoint(); var symbol = state.symbol(owner); var members = symbol instanceof ClassSymbol c ? c.members() : symbol instanceof StructureSymbol s ? s.members() : List.of(); - for (var id : members) if (g.symbols.get(id) instanceof ParameterSymbol p && p.name().value().equals(name)) return Optional.of(id); - owner = g.baseOf.get(owner); + for (var id : members) if (state.symbol(id) instanceof ParameterSymbol p && p.name().value().equals(name)) return Optional.of(id); + owner = state.baseOf(owner); } return Optional.empty(); } @@ -196,39 +249,39 @@ void validateRange(DeclInfo decl, NumericRange range) { if (range.referenceSystem().isEmpty()) return; var ref = range.referenceSystem().get(); Optional requiredUnit = Optional.empty(); if (ref.domain()) { - var domain = (DomainSymbol)g.symbols.get(ref.target()); - var target = domain == null ? null : g.effectiveDescriptor(domain.typeId()); + var domain = (DomainSymbol)state.symbol(ref.target()); + var target = domain == null ? null : typeBindings.get().effectiveDescriptor(domain.typeId()); if (target instanceof NumericType n && ref.axis().isEmpty()) requiredUnit = n.range().unit(); else if (target instanceof CoordinateType c && ref.axis().isPresent() && ref.axis().getAsInt() <= c.dimensions()) requiredUnit = c.axes().get(ref.axis().getAsInt() - 1).unit(); else if (target instanceof CoordinateType c && ref.axis().isEmpty() && (c.rotation().isPresent() || domain.generic())) { // A direction uses the coordinate system's zero direction and sense, not one of its length axes. - if (range.unit().isPresent() && !g.unitExtends(range.unit().get(), g.builtinUnit(decl.id(), "INTERLIS.ANGLE").orElseThrow())) - g.emitRule(decl, "a coordinate direction reference requires an angular unit", "REFSYSTEM_DIRECTION_UNIT"); + if (range.unit().isPresent() && !units.unitExtends(range.unit().get(), units.builtinUnit(decl.id(), "INTERLIS.ANGLE").orElseThrow())) + diagnostics.emitRule(decl, "a coordinate direction reference requires an angular unit", "REFSYSTEM_DIRECTION_UNIT"); } - else g.emitRule(decl, "reference-system domain must be numeric or select an existing coordinate axis", "REFSYSTEM_DOMAIN"); + else diagnostics.emitRule(decl, "reference-system domain must be numeric or select an existing coordinate axis", "REFSYSTEM_DOMAIN"); } else { - var object = (MetaObjectSymbol)g.symbols.get(ref.target()); + var object = (MetaObjectSymbol)state.symbol(ref.target()); if (object == null || object.objectClass().isEmpty()) return; SymbolId cls = object.objectClass().get(); var required = predefined(decl, ref.axis().isPresent() ? "INTERLIS.COORDSYSTEM" : "INTERLIS.REFSYSTEM", Set.of(SymbolKind.CLASS)).orElseThrow(); - if (!specializes(cls, required)) { g.emitRule(decl, "reference-system object has the wrong scalar/coordinate family", "REFSYSTEM_OBJECT"); return; } + if (!specializes(cls, required)) { diagnostics.emitRule(decl, "reference-system object has the wrong scalar/coordinate family", "REFSYSTEM_OBJECT"); return; } if (ref.axis().isPresent()) { - var attribute = g.findMember(cls, "Axis").map(g.symbols::get).orElse(null); - if (attribute instanceof AttributeSymbol a && g.effectiveDescriptor(a.declaredType()) instanceof CollectionType list) { + var attribute = paths.findMember(cls, "Axis").map(state::symbol).orElse(null); + if (attribute instanceof AttributeSymbol a && typeBindings.get().effectiveDescriptor(a.declaredType()) instanceof CollectionType list) { if (list.cardinality().upper() instanceof CardinalityUpper.Finite max && java.math.BigInteger.valueOf(ref.axis().getAsInt()).compareTo(max.value()) > 0) - g.emitRule(decl, "reference-system axis is outside the declared axis list", "REFSYSTEM_AXIS"); - if (g.effectiveDescriptor(list.elementType()) instanceof StructureValueType s) cls = g.structures.target(s); + diagnostics.emitRule(decl, "reference-system axis is outside the declared axis list", "REFSYSTEM_AXIS"); + if (typeBindings.get().effectiveDescriptor(list.elementType()) instanceof StructureValueType s) cls = structures.target(s); } } - var param = parameter(cls, "Unit").map(g.symbols::get).orElse(null); - if (param instanceof ParameterSymbol p && g.effectiveDescriptor(p.typeId()) instanceof NumericType n) requiredUnit = n.range().unit(); + var param = parameter(cls, "Unit").map(state::symbol).orElse(null); + if (param instanceof ParameterSymbol p && typeBindings.get().effectiveDescriptor(p.typeId()) instanceof NumericType n) requiredUnit = n.range().unit(); } - if (requiredUnit.isPresent() && range.unit().isEmpty() && !requiredUnit.get().equals(g.builtinUnit(decl.id(), "INTERLIS.ANYUNIT").orElseThrow())) - g.emitRule(decl, "reference system with a specific Unit requires an explicit numeric unit", "REFSYSTEM_UNIT_REQUIRED"); - if (requiredUnit.isPresent() && range.unit().isPresent() && !g.unitExtends(range.unit().get(), requiredUnit.get()) - && !g.unitsConvertible(range.unit().get(), requiredUnit.get())) - g.emitRule(decl, "numeric unit is incompatible with its reference system", "REFSYSTEM_UNIT"); + if (requiredUnit.isPresent() && range.unit().isEmpty() && !requiredUnit.get().equals(units.builtinUnit(decl.id(), "INTERLIS.ANYUNIT").orElseThrow())) + diagnostics.emitRule(decl, "reference system with a specific Unit requires an explicit numeric unit", "REFSYSTEM_UNIT_REQUIRED"); + if (requiredUnit.isPresent() && range.unit().isPresent() && !units.unitExtends(range.unit().get(), requiredUnit.get()) + && !units.unitsConvertible(range.unit().get(), requiredUnit.get())) + diagnostics.emitRule(decl, "numeric unit is incompatible with its reference system", "REFSYSTEM_UNIT"); } Optional predefined(DeclInfo context, String name, Set kinds) { @@ -237,56 +290,56 @@ Optional predefined(DeclInfo context, String name, Set kin if (!kinds.contains(kind)) return Optional.empty(); var h = header(context.language(), name.substring(9), kind, Optional.empty(), 0, kind == SymbolKind.CLASS && !name.equals("INTERLIS.METAOBJECT_TRANSLATION") ? Set.of(Modifier.ABSTRACT) : Set.of()); - if (g.symbols.containsKey(h.id())) return Optional.of(h.id()); + if (state.hasSymbol(h.id())) return Optional.of(h.id()); String baseName = switch (name) { case "INTERLIS.REFSYSTEM", "INTERLIS.SIGN" -> "INTERLIS.METAOBJECT"; case "INTERLIS.COORDSYSTEM", "INTERLIS.SCALSYSTEM" -> "INTERLIS.REFSYSTEM"; default -> null; }; var base = baseName == null ? Optional.empty() : predefined(context, baseName, Set.of(SymbolKind.CLASS)); - base.ifPresent(id -> g.baseOf.put(h.id(), id)); + base.ifPresent(id -> inheritance.bindBase(h.id(), id)); List members = new ArrayList<>(); List constraints = new ArrayList<>(); if (name.equals("INTERLIS.METAOBJECT") || name.equals("INTERLIS.METAOBJECT_TRANSLATION")) { - var t = g.nextTypeId(); g.types.put(t, new TextType(t, h.origin(), TextKind.NAME, OptionalInt.of(255))); + var t = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new TextType(t, h.origin(), TextKind.NAME, OptionalInt.of(255))); var names = name.equals("INTERLIS.METAOBJECT") ? List.of("Name") : List.of("Name", "NameInBaseLanguage"); for (int i = 0; i < names.size(); i++) { - g.checkWork(); members.add(member(h, names.get(i), t, false, true, i)); + work.checkpoint(); members.add(member(h, names.get(i), t, false, true, i)); } var range = new guru.interlis.source.SourceRange(new guru.interlis.source.SourceId("predefined:/INTERLIS-" + context.language()), guru.interlis.source.SourceRevision.initial(), 0, 0); for (int i = 0; i < names.size(); i++) { - g.checkWork(); + work.checkpoint(); var ch = header(context.language(), name.substring(9) + "." + names.get(i) + "Unique", SymbolKind.CONSTRAINT, Optional.of(h.id()), names.size() + i, Set.of()); var expression = new ExpressionId(ch.id().value() + ":path"); - g.expressions.put(expression, new PathExpression(expression, t, ExpressionTypeState.RESOLVED, range, List.of(), + expressions.get().defineExpression(new PathExpression(expression, t, ExpressionTypeState.RESOLVED, range, List.of(), new ObjectPath(List.of(new PathStep.AttributeStep(names.get(i), members.get(i), range))))); - g.symbols.put(ch.id(), new ConstraintSymbol(ch, new UniquenessConstraint(Optional.empty(), false, Optional.empty(), List.of(expression), List.of()), Optional.empty())); + state.defineSymbol(new ConstraintSymbol(ch, new UniquenessConstraint(Optional.empty(), false, Optional.empty(), List.of(expression), List.of()), Optional.empty())); constraints.add(ch.id()); } } if (name.equals("INTERLIS.AXIS") || name.equals("INTERLIS.SCALSYSTEM")) { - var t = g.nextTypeId(); g.types.put(t, new NumericType(t, h.origin(), new NumericRange(Optional.empty(), Optional.empty(), false, - NumericDirection.NONE, g.builtinUnit(context.id(), "INTERLIS.ANYUNIT"), Optional.empty()))); + var t = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new NumericType(t, h.origin(), new NumericRange(Optional.empty(), Optional.empty(), false, + NumericDirection.NONE, units.builtinUnit(context.id(), "INTERLIS.ANYUNIT"), Optional.empty()))); members.add(member(h, "Unit", t, true, false, 0)); } if (name.equals("INTERLIS.COORDSYSTEM")) { var axis = predefined(context, "INTERLIS.AXIS", Set.of(SymbolKind.STRUCTURE)).orElseThrow(); - var t = g.nextTypeId(); g.types.put(t, new StructureValueType(t, h.origin(), Optional.of(axis), List.of())); - var list = g.nextTypeId(); g.types.put(list, new CollectionType(list, h.origin(), CollectionKind.LIST, Cardinality.of(1, 3), t)); + var t = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new StructureValueType(t, h.origin(), Optional.of(axis), List.of())); + var list = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new CollectionType(list, h.origin(), CollectionKind.LIST, Cardinality.of(1, 3), t)); members.add(member(h, "Axis", list, false, false, 0)); } if (name.equals("INTERLIS.SIGN")) { - var type=g.nextTypeId(); g.types.put(type,new MetaObjectType(type,h.origin(),Optional.empty())); + var type=typeBindings.get().nextTypeId(); typeBindings.get().defineType(new MetaObjectType(type,h.origin(),Optional.empty())); members.add(member(h,"Sign",type,true,false, 0)); } - g.symbols.put(h.id(), kind == SymbolKind.CLASS ? new ClassSymbol(h, base, members, constraints) : new StructureSymbol(h, base, members, constraints)); + state.defineSymbol(kind == SymbolKind.CLASS ? new ClassSymbol(h, base, members, constraints) : new StructureSymbol(h, base, members, constraints)); List effective = new ArrayList<>(); - base.ifPresent(id -> g.effectiveMembers.getOrDefault(id, List.of()).forEach(m -> effective.add(new EffectiveMember(m.declaredSymbol(), h.id(), List.of(id, h.id()), effective.size(), EffectiveMemberOrigin.INHERITED, false)))); - for (var id : members) if (g.symbols.get(id).kind() == SymbolKind.ATTRIBUTE) + base.ifPresent(id -> state.effectiveMembers(id).forEach(m -> effective.add(new EffectiveMember(m.declaredSymbol(), h.id(), List.of(id, h.id()), effective.size(), EffectiveMemberOrigin.INHERITED, false)))); + for (var id : members) if (state.symbol(id).kind() == SymbolKind.ATTRIBUTE) effective.add(new EffectiveMember(id, h.id(), List.of(h.id()), effective.size(), EffectiveMemberOrigin.LOCAL, false)); - g.effectiveMembers.put(h.id(), List.copyOf(effective)); + inheritance.setEffectiveMembers(h.id(), List.copyOf(effective)); return Optional.of(h.id()); } @@ -295,9 +348,9 @@ private SymbolId member(SymbolHeader owner, String name, TypeId type, boolean pa parameter ? SymbolKind.PARAMETER : SymbolKind.ATTRIBUTE, Optional.of(owner.id()), order, Set.of()); var h = new SymbolHeader(original.id(), original.kind(), DeclaredName.declared(name), original.owner(), order, original.origin(), Optional.empty(), original.languageVersion(), Documentation.empty(), MetaAttributeSet.empty(), Set.of(), List.of()); - g.symbols.put(h.id(), parameter ? new ParameterSymbol(h, type, mandatory, Optional.empty()) + state.defineSymbol(parameter ? new ParameterSymbol(h, type, mandatory, Optional.empty()) : new AttributeSymbol(h, type, mandatory, false, false, false, Optional.empty(), List.of())); - g.declaredTypes.put(h.id(), type); return h.id(); + typeBindings.get().bindDeclaredType(h.id(), type); return h.id(); } private SymbolHeader header(LanguageVersion language, String name, SymbolKind kind, Optional owner, int order, Set modifiers) { return new SymbolHeader(new SymbolId("predefined:" + language + ":" + name), kind, owner.isEmpty() ? DeclaredName.synthetic("INTERLIS." + name) : DeclaredName.declared(name), diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NameBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NameBindings.java new file mode 100644 index 00000000..b4791417 --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NameBindings.java @@ -0,0 +1,135 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.diagnostics.DiagnosticArgument; +import guru.interlis.diagnostics.DiagnosticCode; +import guru.interlis.diagnostics.DiagnosticDraft; +import guru.interlis.diagnostics.DiagnosticId; +import guru.interlis.diagnostics.Phase; +import guru.interlis.diagnostics.Severity; +import guru.interlis.model.FunctionSymbol; +import guru.interlis.model.LanguageVersion; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.syntax.NameSyntax; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; + +/** Applies source-level predefined and visibility rules to indexed name resolution. */ +final class NameBindings { + @FunctionalInterface + interface PredefinedLookup { + Optional lookup(DeclInfo owner, String name, Set kinds); + } + + private final IndexedDeclarations index; + private final NameResolver resolver; + private final SemanticWork work; + private final SemanticDiagnostics diagnostics; + private final ReferenceBindings references; + private final PredefinedLookup temporal; + private final PredefinedLookup metadata; + private final PredefinedLookup lines; + private final BiFunction> predefinedOid; + private final BiFunction> predefinedDomain; + private final BiFunction> predefinedUnit; + private final BiFunction predefinedFunction; + + NameBindings(IndexedDeclarations index, NameResolver resolver, SemanticWork work, SemanticDiagnostics diagnostics, + ReferenceBindings references, PredefinedLookup temporal, PredefinedLookup metadata, PredefinedLookup lines, + BiFunction> predefinedOid, + BiFunction> predefinedDomain, + BiFunction> predefinedUnit, + BiFunction predefinedFunction) { + this.index = index; + this.resolver = resolver; + this.work = work; + this.diagnostics = diagnostics; + this.references = references; + this.temporal = temporal; + this.metadata = metadata; + this.lines = lines; + this.predefinedOid = predefinedOid; + this.predefinedDomain = predefinedDomain; + this.predefinedUnit = predefinedUnit; + this.predefinedFunction = predefinedFunction; + } + + /** One source-name selection for user declarations and canonical predefined targets. */ + NameResolver.Resolution resolveName(DeclInfo owner, NameSyntax name, + Set kinds, boolean relativeQualifiers) { + work.checkpoint(); + if(name.text().startsWith("INTERLIS.")) { + var predefined=predefinedTarget(owner,name.text(),kinds); + if(predefined.isPresent())return new NameResolver.Resolution(NameResolver.Status.BOUND,predefined,List.of()); + return resolver.resolve(owner.id(),name,kinds,relativeQualifiers); + } + if(name.segments().size()!=1)return resolver.resolve(owner.id(),name,kinds,relativeQualifiers); + var local=resolver.resolveLocal(owner.id(),name,kinds); + if(local.status()!=NameResolver.Status.UNRESOLVED)return local; + var normal=resolver.resolve(owner.id(),name,kinds,relativeQualifiers); + boolean visible=index.modelOfSymbol(owner.id()).flatMap(index::byId).filter(d->d.syntax() instanceof guru.interlis.syntax.ModelSyntax) + .map(d->((guru.interlis.syntax.ModelSyntax)d.syntax()).imports().stream().anyMatch(i->i.modelName().equals("INTERLIS") && i.unqualified())).orElse(false); + // Preserve the existing priority of actual imported declarations. In particular, + // ambiguity is a result, never a reason to substitute a builtin function. + if(!visible || normal.status()!=NameResolver.Status.UNRESOLVED)return normal; + var predefined=predefinedTarget(owner,"INTERLIS."+name.text(),kinds); + return predefined.isPresent() + ? new NameResolver.Resolution(NameResolver.Status.BOUND,predefined,List.of()) : normal; + } + + private Optional predefinedTarget(DeclInfo owner,String qualified,Set kinds) { + if(!qualified.startsWith("INTERLIS."))return Optional.empty(); + Optional target=kinds.contains(SymbolKind.DOMAIN)?predefinedOid.apply(owner.id(),qualified):Optional.empty(); + if(target.isEmpty() && kinds.contains(SymbolKind.DOMAIN))target=predefinedDomain.apply(owner,qualified); + if(target.isEmpty())target=temporal.lookup(owner,qualified,kinds); + if(target.isEmpty())target=metadata.lookup(owner,qualified,kinds); + if(target.isEmpty())target=lines.lookup(owner,qualified,kinds); + if(target.isEmpty() && kinds.contains(SymbolKind.UNIT))target=predefinedUnit.apply(owner.id(),qualified); + if(target.isEmpty() && kinds.contains(SymbolKind.FUNCTION) && qualified.indexOf('.',9)<0) { + var signature=BuiltinFunctions.find(qualified.substring(9)).filter(f->f.since()!=LanguageVersion.ILI24 || owner.language()==LanguageVersion.ILI24); + if(signature.isPresent())target=Optional.of(predefinedFunction.apply(signature.orElseThrow(),owner.language()).id()); + } + return target; + } + + Optional bindTarget(DeclInfo owner, NameSyntax name, + Set kinds, ReferenceUsage usage, List errors) { + return bindTarget(owner, name, kinds, usage, errors, true); + } + + Optional bindTarget(DeclInfo owner, NameSyntax name, + Set kinds, ReferenceUsage usage, List errors, boolean relativeQualifiers) { + var resolution = resolveName(owner, name, kinds, relativeQualifiers); + Optional target = resolution.target(); + references.recordResolution(owner, name, resolution, usage); + if (resolution.status() == NameResolver.Status.AMBIGUOUS) { + errors.add(reportAmbiguity(name, resolution)); + } else if (target.isEmpty()) { + errors.add(diagnostics.unresolved(name)); + } + return target; + } + + DiagnosticId reportAmbiguity(NameSyntax name, NameResolver.Resolution resolution) { + return diagnostics.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0002"), + Severity.ERROR, Phase.BINDING, "diagnostic.semantics.ambiguous") + .at(name.range()) + .argument(new DiagnosticArgument.IdentifierArgument("name", name.text())) + .argument(new DiagnosticArgument.IdentifierListArgument("candidates", + resolution.candidates().stream().map(SymbolId::value).toList()))); + } + + Optional tryResolve(DeclInfo context, + NameSyntax name, + Set expected) { + NameResolver.Resolution resolution = resolver.resolve(context.id(), name, expected); + return resolution.status() == NameResolver.Status.BOUND + ? resolution.target() : Optional.empty(); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NameResolver.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NameResolver.java index 1791139c..eacd4cc8 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NameResolver.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NameResolver.java @@ -109,7 +109,7 @@ Resolution resolve(SymbolId owner, NameSyntax name, Set expected, bo return new Resolution(Status.AMBIGUOUS, Optional.empty(), ordered); } - /** Indexed lexical declarations only; predefined values remain owned by GraphBuilder. */ + /** Indexed lexical declarations only; predefined values remain owned by the predefined binders. */ Resolution resolveLocal(SymbolId owner, NameSyntax name, Set expected) { if(name.segments().size()!=1)return new Resolution(Status.UNRESOLVED,Optional.empty(),List.of()); String simple=name.segments().getFirst().text(); diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NumericSubdivisions.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NumericSubdivisions.java index 5aa3f70e..18ddb6a7 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NumericSubdivisions.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/NumericSubdivisions.java @@ -1,40 +1,58 @@ package guru.interlis.compiler.internal; import guru.interlis.model.*; + import java.math.BigDecimal; import java.util.*; +import java.util.function.Function; /** Contextual subdivision rules over the final, alias-resolved effective attribute order. */ final class NumericSubdivisions { - private final GraphBuilder graph; + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final Function effectiveDescriptor; private final ExactUnitFactors factors; - NumericSubdivisions(GraphBuilder graph) { this.graph=graph;this.factors=new ExactUnitFactors(graph); } + + NumericSubdivisions(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + Function effectiveDescriptor) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.effectiveDescriptor = effectiveDescriptor; + this.factors = new ExactUnitFactors(state, work); + } void validate() { Map> originalPredecessors=new HashMap<>(); - for(var owner:graph.effectiveMembers.entrySet()) { + for(var owner:state.effectiveMembersInBuildOrder()) { Optional previous=Optional.empty(); for(var member:owner.getValue()) { - graph.checkWork(); - if(!(graph.symbols.get(member.declaredSymbol()) instanceof AttributeSymbol attribute))continue; + work.checkpoint(); + if(!(state.symbol(member.declaredSymbol()) instanceof AttributeSymbol attribute))continue; if(attribute.owner().filter(owner.getKey()::equals).isPresent())originalPredecessors.put(attribute.id(),previous); previous=Optional.of(attribute.id()); } } record Relation(SymbolId attribute,Optional previous) { } Set checked=new HashSet<>(); - for(var owner:graph.effectiveMembers.entrySet()) { - graph.checkWork();AttributeSymbol previous=null; + for(var owner:state.effectiveMembersInBuildOrder()) { + work.checkpoint();AttributeSymbol previous=null; for(var member:owner.getValue()) { - graph.checkWork(); - if(!(graph.symbols.get(member.declaredSymbol()) instanceof AttributeSymbol attribute))continue; + work.checkpoint(); + if(!(state.symbol(member.declaredSymbol()) instanceof AttributeSymbol attribute))continue; if(attribute.subdivision()) { var preceding=previous==null?Optional.empty():Optional.of(previous.id()); var relation=new Relation(attribute.id(),preceding); boolean unchanged=preceding.equals(originalPredecessors.get(attribute.id())); - var declaration=unchanged?graph.index.byId(attribute.id()).orElse(null) - :previous==null?null:previous.owner().flatMap(graph.index::byId).orElse(null); - if(declaration==null)declaration=graph.index.byId(owner.getKey()).orElse(null); + var declaration=unchanged?index.byId(attribute.id()).orElse(null) + :previous==null?null:previous.owner().flatMap(index::byId).orElse(null); + if(declaration==null)declaration=index.byId(owner.getKey()).orElse(null); // Predefined declarations have no user source. Their inherited // uses are checked in the context of each declared extension. if(declaration!=null&&checked.add(relation))validate(attribute,previous,declaration); @@ -45,8 +63,8 @@ record Relation(SymbolId attribute,Optional previous) { } } private void validate(AttributeSymbol attribute,AttributeSymbol previous,DeclInfo declaration) { if(previous==null) { error(declaration,"subdivision needs a preceding attribute","SUBDIVISION_PREDECESSOR");return; } - var own=graph.effectiveDescriptor(attribute.declaredType()); - var predecessor=graph.effectiveDescriptor(previous.declaredType()); + var own=effectiveDescriptor.apply(attribute.declaredType()); + var predecessor=effectiveDescriptor.apply(previous.declaredType()); if(unknown(own)||unknown(predecessor))return; if(!(own instanceof NumericType number)) {error(declaration,"subdivision must have a scalar numeric type","SUBDIVISION_NUMERIC");return;} if(!(predecessor instanceof NumericType parent)) {error(declaration,"subdivision predecessor must have a scalar numeric type","SUBDIVISION_PREDECESSOR_NUMERIC");return;} @@ -62,16 +80,16 @@ private void validate(AttributeSymbol attribute,AttributeSymbol previous,DeclInf if(!number.range().circular()) {error(declaration,"continuous subdivision must be CIRCULAR","SUBDIVISION_CIRCULAR");return;} if(number.range().minimum().isEmpty()||number.range().maximum().isEmpty() ||parent.range().unit().isEmpty()||number.range().unit().isEmpty())return; - var parentUnit=graph.symbols.get(parent.range().unit().orElseThrow()); + var parentUnit=state.symbol(parent.range().unit().orElseThrow()); if(parentUnit instanceof UnitSymbol unit && unit.abstractUnit()) { var specialization=guru.interlis.model.internal.TypeCompatibility.numericUnitExtension(number,parent,id->{ - graph.checkWork();return graph.symbols.get(id) instanceof UnitSymbol value?value:null; + work.checkpoint();return state.symbol(id) instanceof UnitSymbol value?value:null; }); if(specialization.decision()==RelationDecision.DISALLOWED) error(declaration,"subdivision unit cannot specialize the abstract predecessor unit","SUBDIVISION_FACTOR"); return; // The concrete predecessor unit, and hence its factor, is still deferred. } - graph.checkWork();var maximum=number.range().maximum().orElseThrow(); + work.checkpoint();var maximum=number.range().maximum().orElseThrow(); var quantum=BigDecimal.ONE.scaleByPowerOfTen(maximum.precision().exponent()-maximum.precision().fractionalDigits()); var minimum=number.range().minimum().orElseThrow().exactValue(); chargeAlignment(minimum,maximum.exactValue(),quantum); @@ -86,9 +104,9 @@ private void chargeAlignment(BigDecimal minimum,BigDecimal maximum,BigDecimal qu long digits=Math.max(Math.max((long)minimum.precision()+scale-minimum.scale(), (long)maximum.precision()+scale-maximum.scale()),1L+scale-quantum.scale()); long words=1+digits/16,work=1+words*words; - for(long i=0;i=value.precision().fractionalDigits();} - private void error(DeclInfo declaration,String message,String reason) {graph.emitRule(declaration,message,reason);} + private void error(DeclInfo declaration,String message,String reason) {diagnostics.emitRule(declaration,message,reason);} } diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ObjectPathBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ObjectPathBindings.java new file mode 100644 index 00000000..26603b33 --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ObjectPathBindings.java @@ -0,0 +1,616 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.compiler.SourceSet; +import guru.interlis.compiler.internal.ExpressionBindings.ExpressionResult; +import guru.interlis.diagnostics.DiagnosticArgument; +import guru.interlis.diagnostics.DiagnosticCode; +import guru.interlis.diagnostics.DiagnosticDraft; +import guru.interlis.diagnostics.DiagnosticId; +import guru.interlis.diagnostics.Phase; +import guru.interlis.diagnostics.Severity; +import guru.interlis.execution.CancellationToken; +import guru.interlis.model.Cardinality; +import guru.interlis.model.ClassSymbol; +import guru.interlis.model.CollectionType; +import guru.interlis.model.CoordinateType; +import guru.interlis.model.ErrorType; +import guru.interlis.model.ExpressionId; +import guru.interlis.model.GeometryType; +import guru.interlis.model.NumericType; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceType; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.RoleSymbol; +import guru.interlis.model.StructureSymbol; +import guru.interlis.model.StructureValueType; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; +import guru.interlis.model.TypedExpression; +import guru.interlis.model.ViewSymbol; +import guru.interlis.source.SourceRange; +import guru.interlis.syntax.AssociationDeclarationSyntax; +import guru.interlis.syntax.ConstraintDeclarationSyntax; +import guru.interlis.syntax.ExpressionSyntax; +import guru.interlis.syntax.ExternalConstraintSyntax; +import guru.interlis.syntax.PathExpressionSyntax; +import guru.interlis.syntax.TopicSyntax; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Binds object paths with explicit value, inspection and function contexts. */ +final class ObjectPathBindings { + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameResolver resolver; + private final SourceSet sourceSet; + private final CancellationToken cancellation; + private final Supplier expressions; + private final Supplier views; + private final Supplier typeBindings; + private final InheritanceBindings inheritance; + private final ReferenceBindings references; + private final StructureBindings structures; + private final Function graphicContext; + private final Function ensureSymbol; + + ObjectPathBindings(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameResolver resolver, + SourceSet sourceSet, + CancellationToken cancellation, + Supplier expressions, + Supplier views, + Supplier typeBindings, + InheritanceBindings inheritance, + ReferenceBindings references, + StructureBindings structures, + Function graphicContext, + Function ensureSymbol) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.resolver = resolver; + this.sourceSet = sourceSet; + this.cancellation = cancellation; + this.expressions = expressions; + this.views = views; + this.typeBindings = typeBindings; + this.inheritance = inheritance; + this.references = references; + this.structures = structures; + this.graphicContext = graphicContext; + this.ensureSymbol = ensureSymbol; + } + + ExpressionResult pathExpression(ExpressionId id, PathExpressionSyntax syntax, + SymbolId owner) { + if (isSetCondition(owner, syntax.range())) { + work.checkpoint(); + var name=syntax.segments().getFirst().name(); + var diagnostic=diagnostics.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0008"),Severity.ERROR, + Phase.TYPE_CHECKING,"diagnostic.semantics.path") + .at(name.range()).argument(new DiagnosticArgument.IdentifierArgument("segment",name.text())) + .argument(new DiagnosticArgument.TextArgument("reason","SET condition has no individual object context; object values belong in WHERE"))); + return expressions.get().failedValueExpression(id,syntax.range(),List.of(diagnostic)); + } + return pathExpression(id, syntax, owner, expressions.get().expressionContext(owner), false); + } + + /** The source-bound main expression distinguishes SET's set context from + * its individual WHERE context, including nested calls and external blocks. + * Explicit class/attribute constants and separately bound function string + * paths do not enter this ordinary object-value path entry point. */ + private boolean isSetCondition(SymbolId owner,SourceRange range) { + var declaration=index.byId(owner).orElseThrow().syntax(); + var constraint=declaration instanceof ExternalConstraintSyntax external?external.constraint() + :declaration instanceof ConstraintDeclarationSyntax local?local:null; + if(constraint==null||constraint.constraintKind()!=guru.interlis.syntax.ConstraintKind.SET)return false; + return constraint.expression().map(ExpressionSyntax::range).filter(body->body.source().equals(range.source()) + &&body.revision().equals(range.revision())&&body.startOffset()<=range.startOffset()&&body.endOffset()>=range.endOffset()).isPresent(); + } + + ExpressionResult pathExpression(ExpressionId id, PathExpressionSyntax syntax, + SymbolId owner, SymbolId start, boolean localPrefix) { + return pathExpression(id, syntax, owner, start, localPrefix, PathContext.VALUE); + } + + guru.interlis.syntax.FragmentSnapshot parsePathLiteral(SourceRange range, guru.interlis.model.LanguageVersion language) { + var fragment = new guru.interlis.compiler.internal.antlr.AntlrSyntaxService().parsePathLiteral( + sourceSet.sources().get(range.source()), range, language, cancellation); + diagnostics.importTypeFragmentDiagnostics(fragment.diagnostics()); + return fragment; + } + + ExpressionResult valuePath(PathExpressionSyntax syntax, SymbolId owner, SymbolId start) { + return pathExpression(expressions.get().nextExpressionId(), syntax, owner, start, false, PathContext.FUNCTION_PATH); + } + + ExpressionResult inspectionPath(PathExpressionSyntax syntax, SymbolId owner, SymbolId start) { + return pathExpression(expressions.get().nextExpressionId(), syntax, owner, start, false, PathContext.INSPECTION); + } + + ExpressionResult pathExpression(ExpressionId id, PathExpressionSyntax syntax, + SymbolId owner, SymbolId start, boolean localPrefix, PathContext pathContext) { + boolean inspection=pathContext==PathContext.INSPECTION; + List steps = new ArrayList<>(); + SymbolId currentScope = start; + SymbolId contextTopic = scopeRoot(graphicContext.apply(owner)); + currentScope = contextualObject(currentScope, contextTopic); + TypeId currentType = null; + boolean error = false; + for (int i = 0; i < syntax.segments().size(); i++) { + work.checkpoint(); + var segment = syntax.segments().get(i); + guru.interlis.syntax.NameSyntax name = segment.name(); + String text = name.text(); + if (Set.of("PARENT", "THISAREA", "THATAREA").contains(text)) { + var inspected = views.get().inspection(expressions.get().expressionContext(owner)); + boolean valid = inspected != null && (text.equals("PARENT") != inspected.area()) + && segment.index().isEmpty() && !segment.associationAccess() && segment.associationQualifier().isEmpty() + && (i == 0 || java.util.Objects.equals(currentScope, inspected.element()) + || java.util.Objects.equals(currentScope, expressions.get().expressionContext(owner))); + if (!valid) { + var diagnostic = diagnostics.emitRule(index.byId(owner).orElseThrow(), "invalid inspection path context", "VIEW_INSPECTION_CONTEXT"); + currentType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new ErrorType(currentType, new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); + steps.add(new guru.interlis.model.PathStep.UnresolvedStep(text, name.range())); + error = true; break; + } + currentScope = inspected.parent(); currentType = objectPathType(currentScope, name.range()); + steps.add(new guru.interlis.model.PathStep.InspectionStep(guru.interlis.model.PathStep.InspectionKind.valueOf(text), + expressions.get().expressionContext(owner), currentScope, name.range())); + continue; + } + if ("THIS".equals(text) && segment.index().isEmpty() && !segment.associationAccess() && (currentScope != null || currentType != null + && typeBindings.get().effectiveDescriptor(currentType) instanceof guru.interlis.model.ObjectArgumentType)) { + steps.add(new guru.interlis.model.PathStep.ThisStep(name.range())); + currentType = i == 0 ? state.declaredType(currentScope) : currentType; + if (currentType == null) { + currentType = objectPathType(currentScope, name.range()); + } + TypeDescriptor descriptor = typeBindings.get().effectiveDescriptor(currentType); + if (!(descriptor instanceof ReferenceType) + && !(descriptor instanceof StructureValueType) + && !(descriptor instanceof guru.interlis.model.ObjectArgumentType)) { + currentScope = null; + } + continue; + } + boolean incomingMany = currentType != null && typeBindings.get().effectiveDescriptor(currentType) + instanceof guru.interlis.model.ObjectArgumentType set && set.objects(); + Optional member = currentScope == null || segment.associationAccess() || segment.associationQualifier().isPresent() + ? Optional.empty() : findMember(currentScope, text); + // Projections retain role access to their source object, including + // roles which ALL OF does not copy as transfer attributes. + if (i == 0 && member.isEmpty()) { + var projection = views.get().projectionBase(currentScope); + if (projection != null) { + currentScope = projection.target(); + steps.add(new guru.interlis.model.PathStep.ViewBaseStep(projection.id(), currentScope, name.range())); + if (!segment.associationAccess() && segment.associationQualifier().isEmpty()) + member = findMember(currentScope, text).filter(candidate -> + index.byId(candidate).map(d -> d.kind() == SymbolKind.ROLE).orElse(false)); + } + } + List objectScopes = currentScope == null ? List.of() : List.of(currentScope); + if (currentType != null && typeBindings.get().effectiveDescriptor(currentType) instanceof guru.interlis.model.ObjectArgumentType objects) { + if (!objects.restrictions().isEmpty()) objectScopes = objects.restrictions(); + else if (objects.base().isPresent()) objectScopes = List.of(objects.base().orElseThrow()); + } + if (!localPrefix && segment.index().isEmpty() && member.isEmpty() && !objectScopes.isEmpty()) { + boolean outgoing = segment.associationQualifier().isPresent(); + List accesses = associationAccesses(objectScopes, contextTopic, name, i == 0, outgoing); + if (!outgoing && !segment.associationAccess() && accesses.isEmpty()) { + accesses = associationAccesses(objectScopes, contextTopic, name, i == 0, true); + } + if (segment.associationQualifier().isPresent()) { + var qualifier = segment.associationQualifier().orElseThrow(); + var resolution = resolver.resolve(owner, qualifier, Set.of(SymbolKind.ASSOCIATION)); + if (resolution.status() == NameResolver.Status.UNRESOLVED) { + var inherited = inheritance.baseTopicMember(owner, qualifier.text(), Set.of(SymbolKind.ASSOCIATION)); + if (inherited.isPresent()) { + resolution = new NameResolver.Resolution(NameResolver.Status.BOUND, inherited, List.of()); + } + } + references.recordResolution(index.byId(owner).orElseThrow(), qualifier, resolution, ReferenceUsage.EXPRESSION); + if (resolution.target().isEmpty()) diagnostics.unresolved(qualifier); + SymbolId selected = resolution.target().orElse(null); + accesses = accesses.stream().filter(a -> a.association().equals(selected)).toList(); + } + if (accesses.size() == 1) { + RoleAccess access = accesses.getFirst(); + boolean associationObject = segment.associationAccess() || access.target().isEmpty(); + currentScope = associationObject ? access.association() + : commonRoleScope(access.target().orElseThrow(), contextTopic); + Cardinality cardinality = accessCardinality(access, associationObject); + boolean many = incomingMany || isMany(cardinality); + currentType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(associationObject + ? new guru.interlis.model.ObjectArgumentType(currentType, new Origin.DeclaredOrigin(segment.range()), + Optional.of(currentScope), List.of(), many) + : rolePathType(currentType, access.target().orElseThrow(), contextTopic, segment.range(), many)); + if (access.target().isEmpty()) { + steps.add(new guru.interlis.model.PathStep.AssociationSourceStep(text, access.association(), + access.source().id(), cardinality, currentType, segment.range())); + } else { + steps.add(new guru.interlis.model.PathStep.AssociationAccessStep(text, + access.association(), access.source().id(), access.target().orElseThrow().id(), + associationObject, cardinality, currentType, segment.range())); + } + references.recordReference(index.byId(owner).orElseThrow(), name, + access.target().orElse(access.source()).id(), ReferenceUsage.EXPRESSION); + continue; + } else if (accesses.size() > 1) { + var diagnostic = diagnostics.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0002"), + Severity.ERROR, Phase.BINDING, "diagnostic.semantics.ambiguous") + .at(name.range()).argument(new DiagnosticArgument.IdentifierArgument("name", text)) + .argument(new DiagnosticArgument.IdentifierListArgument("candidates", + accesses.stream().map(a -> a.association().value() + ":" + a.source().id().value()).toList()))); + // Distinct accesses may still lead to the same declared role. + var candidates = accesses.stream().map(access -> access.target().orElse(access.source()).id()) + .distinct().sorted(Comparator.comparing(SymbolId::value)).toList(); + references.recordResolution(index.byId(owner).orElseThrow(), name, + new NameResolver.Resolution(NameResolver.Status.AMBIGUOUS, Optional.empty(), candidates), + ReferenceUsage.EXPRESSION); + currentType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new ErrorType(currentType, new Origin.DeclaredOrigin(segment.range()), List.of(diagnostic))); + steps.add(new guru.interlis.model.PathStep.UnresolvedStep(text, segment.range())); + error = true; + break; + } + } + if (member.isEmpty()) { + DiagnosticId diagnostic = diagnostics.unresolved(name); + steps.add(new guru.interlis.model.PathStep.UnresolvedStep(text, name.range())); + TypeId errorType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new ErrorType(errorType, + new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); + currentType = errorType; + error = true; + break; + } + var referenceTarget = state.symbol(member.orElseThrow()); + references.recordReference(index.byId(owner).orElseThrow(), name, + referenceTarget instanceof ViewSymbol.Base basis && basis.nameRange().isEmpty() ? basis.target() : member.orElseThrow(), + ReferenceUsage.EXPRESSION); + SymbolKind kind = index.byId(member.get()).map(DeclInfo::kind).orElse(SymbolKind.ATTRIBUTE); + if ((localPrefix || segment.index().isPresent()) && kind != SymbolKind.ATTRIBUTE) { + DiagnosticId diagnostic = diagnostics.emitTypeMismatch(expressions.get().ownerSource(owner), name.range(), "role", "structure attribute"); + currentType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new ErrorType(currentType, new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); + error = true; + break; + } + if (kind == SymbolKind.VIEW_BASE) { + ViewSymbol.Base base = (ViewSymbol.Base) ensureSymbol.apply(index.byId(member.get()).orElseThrow()); + steps.add(new guru.interlis.model.PathStep.ViewBaseStep(base.id(), views.get().basisTarget(base), name.range())); + currentScope = views.get().basisTarget(base); currentType = objectPathType(currentScope, name.range()); + } else if (kind == SymbolKind.ROLE) { + steps.add(new guru.interlis.model.PathStep.RoleStep(text, member.get(), + name.range())); + // A role declared on this association selects one participant of + // the association instance. Its relationship cardinality does + // not turn that participant into an object set. + RoleSymbol role = (RoleSymbol) ensureSymbol.apply(index.byId(member.get()).orElseThrow()); + currentScope = commonRoleScope(role, contextTopic); + currentType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(rolePathType(currentType, role, contextTopic, name.range(), incomingMany)); + } else { + steps.add(new guru.interlis.model.PathStep.AttributeStep(text, member.get(), + name.range())); + currentType = state.declaredType(member.get()); + if (currentType == null) currentType = typeBindings.get().attributeType(index.byId(member.get()).orElseThrow()); + TypeDescriptor descriptor = currentType == null ? null + : typeBindings.get().effectiveDescriptor(currentType); + currentScope = null; + if (segment.index().isPresent()) { + var selection = segment.index().orElseThrow(); + TypeId selected = incomingMany ? null : indexedType(descriptor, selection, segment.range()); + if (selected == null) { + DiagnosticId diagnostic = diagnostics.emitTypeMismatch(expressions.get().ownerSource(owner), segment.range(), + typeBindings.get().typeName(currentType), "valid LIST position or coordinate axis"); + currentType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new ErrorType(currentType, new Origin.DeclaredOrigin(segment.range()), List.of(diagnostic))); + error = true; + break; + } + currentType = selected; + descriptor = typeBindings.get().effectiveDescriptor(currentType); + steps.add(new guru.interlis.model.PathStep.IndexStep(selection, currentType, segment.range())); + } + if (localPrefix) { + currentScope = structures.target(descriptor); + if (currentScope == null) { + DiagnosticId diagnostic = diagnostics.emitTypeMismatch(expressions.get().ownerSource(owner), name.range(), + typeBindings.get().typeName(currentType), "structure attribute"); + currentType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new ErrorType(currentType, new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); + error = true; + break; + } + } else if (descriptor instanceof CollectionType collection && !incomingMany + && (pathContext!=PathContext.VALUE || !isMany(collection.cardinality())) && i + 1 < syntax.segments().size()) { + currentScope = structures.target(collection); + } else if (descriptor instanceof ReferenceType reference) { + List roots = reference.targetRoots().stream().map(t -> contextualObject(t, contextTopic)).toList(); + currentScope = commonObjectScope(roots); + currentType = typeBindings.get().nextTypeId(); + if (incomingMany) { + typeBindings.get().defineType(new guru.interlis.model.ObjectArgumentType(currentType, + new Origin.DeclaredOrigin(name.range()), roots.size() == 1 ? Optional.of(roots.getFirst()) : Optional.empty(), + roots.size() <= 1 ? List.of() : roots, true)); + } else { + typeBindings.get().defineType(new ReferenceType(currentType, new Origin.DeclaredOrigin(name.range()), + reference.target().map(t -> contextualObject(t, contextTopic)), + reference.restrictions().stream().map(t -> contextualObject(t, contextTopic)).toList(), reference.external())); + } + } else if (inspection && descriptor instanceof GeometryType geometry && i + 1 < syntax.segments().size()) { + currentScope = views.get().geometryElement(index.byId(owner).orElseThrow(), geometry, false); + } else if (!incomingMany && descriptor instanceof StructureValueType structure) { + currentScope = structures.target(structure); + } else if (!incomingMany && descriptor instanceof guru.interlis.model.ObjectArgumentType object) { + // Derived view values can retain a participant object type. + // Continue at its actual roots just as for a bound role. + var roots = object.restrictions().isEmpty() ? object.base().stream().toList() : object.restrictions(); + currentScope = commonObjectScope(roots.stream().map(t -> contextualObject(t, contextTopic)).toList()); + } else if (incomingMany) { + DiagnosticId diagnostic = diagnostics.emitTypeMismatch(expressions.get().ownerSource(owner), name.range(), + "multiple attribute values", "single attribute value"); + currentType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new ErrorType(currentType, new Origin.DeclaredOrigin(name.range()), List.of(diagnostic))); + error = true; + break; + } + } + } + if (currentType == null) { + currentType = typeBindings.get().errorType(expressions.get().ownerSource(owner), syntax.range(), "PATH"); + error = true; + } + guru.interlis.model.ObjectPath path = new guru.interlis.model.ObjectPath(steps.isEmpty() + ? List.of(new guru.interlis.model.PathStep.ThisStep(syntax.range())) : steps); + TypedExpression expression = new guru.interlis.model.PathExpression(id, currentType, + error ? guru.interlis.model.ExpressionTypeState.ERROR + : guru.interlis.model.ExpressionTypeState.RESOLVED, + syntax.range(), List.of(), path); + expressions.get().defineExpression(expression); + return new ExpressionResult(id, currentType, error); + } + + private TypeId indexedType(TypeDescriptor type, guru.interlis.model.PathIndex index, + guru.interlis.source.SourceRange range) { + var position = index.position(); + if (position.filter(p -> p.signum() <= 0).isPresent()) return null; + if (type instanceof CollectionType collection && collection.collectionKind() == guru.interlis.model.CollectionKind.LIST) { + if (collection.cardinality().upper() instanceof guru.interlis.model.CardinalityUpper.Finite maximum + && (maximum.value().signum() == 0 || position.filter(p -> p.compareTo(maximum.value()) > 0).isPresent())) + return null; + return collection.elementType(); + } + if (type instanceof CoordinateType coordinate && !coordinate.multiCoordinate() && position.isPresent() + && position.orElseThrow().compareTo(java.math.BigInteger.valueOf(coordinate.dimensions())) <= 0) { + TypeId result = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new NumericType(result, new Origin.DeclaredOrigin(range), + coordinate.axes().get(position.orElseThrow().intValueExact() - 1))); + return result; + } + return null; + } + + boolean isMany(Cardinality cardinality) { + return !(cardinality.upper() instanceof guru.interlis.model.CardinalityUpper.Finite finite) + || finite.value().compareTo(java.math.BigInteger.ONE) > 0; + } + + private Cardinality accessCardinality(RoleAccess access, boolean associationObject) { + var roles = inheritance.associationRoles(access.association()); + // A role bound only for one participant does not constrain the remaining + // combinations in an n-ary relation. Never infer a singleton from it. + if (roles.size() != 2) return Cardinality.unboundedFrom(0); + RoleSymbol target = access.target().orElseGet(() -> roles.stream() + .filter(r -> !r.id().equals(access.source().id())).findFirst().orElseThrow()); + Cardinality cardinality = target.cardinality(); + if (associationObject) { + Cardinality links = Cardinality.exactly(1); + for (SymbolId base : inheritance.baseChain(access.association())) { + var syntax = (AssociationDeclarationSyntax) index.byId(base).orElseThrow().syntax(); + if (syntax.cardinality().isPresent()) { links = syntax.cardinality().orElseThrow(); break; } + } + cardinality = multiplyCardinality(cardinality, links); + } + return cardinality; + } + + private Cardinality multiplyCardinality(Cardinality left, Cardinality right) { + var upper = left.upper() instanceof guru.interlis.model.CardinalityUpper.Finite l + && right.upper() instanceof guru.interlis.model.CardinalityUpper.Finite r + ? new guru.interlis.model.CardinalityUpper.Finite(l.value().multiply(r.value())) + : guru.interlis.model.CardinalityUpper.unbounded(); + return new Cardinality(left.lower().multiply(right.lower()), upper); + } + + private List associationAccesses(List fromTargets, SymbolId contextTopic, + guru.interlis.syntax.NameSyntax name, boolean first, boolean outgoing) { + List result = new ArrayList<>(); + for (DeclInfo association : index.all()) { + work.checkpoint(); + if (association.kind() != SymbolKind.ASSOCIATION) continue; + SymbolId topic = scopeRoot(association.id()); + // Traversing a role or projecting an object retains the role namespace + // of that reached object, in addition to the declaring context. + if (!topicAvailable(contextTopic, topic) + && !fromTargets.stream().allMatch(from -> topicAvailable(scopeRoot(from), topic))) continue; + // A declaration appearing later cannot retroactively add an access to a constraint. + if (association.sourceId().equals(name.range().source()) + && association.syntax().range().startOffset() > name.range().startOffset()) continue; + List roles = inheritance.associationRoles(association.id()); + for (RoleSymbol target : roles) { + work.checkpoint(); + if (!outgoing && !target.name().value().equals(name.text())) continue; + for (RoleSymbol source : roles) { + work.checkpoint(); + if (outgoing ? !source.id().equals(target.id()) : source.id().equals(target.id())) continue; + if (outgoing && !source.name().value().equals(name.text())) continue; + if (!fromTargets.stream().allMatch(from -> inheritance.roleAccepts(source, from))) continue; + if (first) { + if (source.external()) continue; + if (!fromTargets.stream().allMatch(from -> { + boolean originalAccess = source.targetRoots().stream() + .anyMatch(t -> inheritance.targetSpecializes(from, t) && Objects.equals(scopeRoot(source.id()), scopeRoot(t))); + boolean extendedHere = Objects.equals(topic, scopeRoot(from)) + && index.byId(from).map(d -> d.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)).orElse(false); + return originalAccess || extendedHere; + })) continue; + } + result.add(new RoleAccess(association.id(), source, outgoing ? Optional.empty() : Optional.of(target))); + } + } + } + List candidates = List.copyOf(result); + result.removeIf(candidate -> candidates.stream().anyMatch(other -> { + if (candidate == other) return false; + boolean derived = inheritance.baseChain(other.association()).contains(candidate.association()); + boolean base = inheritance.baseChain(candidate.association()).contains(other.association()); + boolean localExtension = index.byId(other.association()).orElseThrow().modifiers() + .contains(guru.interlis.model.Modifier.EXTENDED); + boolean candidateExtension = index.byId(candidate.association()).orElseThrow().modifiers() + .contains(guru.interlis.model.Modifier.EXTENDED); + return derived && (other.source().hiding() || localExtension) + || base && !candidate.source().hiding() && !candidateExtension; + })); + return result; + } + + boolean topicAvailable(SymbolId context, SymbolId candidate) { + Set seen = new HashSet<>(); + for (SymbolId current = context; current != null && seen.add(current);) { + work.checkpoint(); + if (current.equals(candidate)) return true; + DeclInfo declaration = index.byId(current).orElse(null); + if (declaration == null || !(declaration.syntax() instanceof TopicSyntax topic) + || topic.baseTopic().isEmpty()) return false; + current = resolver.resolve(current, topic.baseTopic().orElseThrow(), Set.of(SymbolKind.TOPIC)) + .target().orElse(null); + } + return false; + } + + SymbolId contextualObject(SymbolId target, SymbolId topic) { + if (target == null) return null; + Set seen = new HashSet<>(); + for (SymbolId context = topic; context != null && seen.add(context); context = state.baseOf(context)) { + for (DeclInfo candidate : index.children(context)) { + work.checkpoint(); + if ((candidate.kind() == SymbolKind.CLASS || candidate.kind() == SymbolKind.ASSOCIATION) + && candidate.modifiers().contains(guru.interlis.model.Modifier.EXTENDED) + && guru.interlis.model.internal.TypeCompatibility.specializes(candidate.id(), target, state::baseOf)) { + return candidate.id(); + } + } + } + return target; + } + + private SymbolId commonRoleScope(RoleSymbol role, SymbolId topic) { + List roots = role.targetRoots().stream().map(t -> contextualObject(t, topic)).toList(); + return commonObjectScope(roots); + } + + SymbolId commonObjectScope(List roots) { + if (roots.isEmpty()) return null; + // Only a common hierarchy admits a subsequent statically bound member. + for (SymbolId candidate : inheritance.baseChain(roots.getFirst())) { + if (roots.stream().allMatch(t -> inheritance.targetSpecializes(t, candidate))) return candidate; + } + return null; + } + + private guru.interlis.model.ObjectArgumentType rolePathType(TypeId id, RoleSymbol role, + SymbolId topic, SourceRange range, boolean many) { + List roots = role.targetRoots().stream().map(t -> contextualObject(t, topic)).toList(); + boolean any = roots.stream().anyMatch(t -> inheritance.objectBase(t).isEmpty()); + return new guru.interlis.model.ObjectArgumentType(id, new Origin.DeclaredOrigin(range), + !any && roots.size() == 1 ? Optional.of(roots.getFirst()) : Optional.empty(), + any || roots.size() == 1 ? List.of() : roots, many); + } + + /** Same participant-value type as an ordinary role path, for implicit 2.4 + * ALL OF values. One basis row denotes one link, not all its peer links. */ + TypeId roleValueType(RoleSymbol role,SymbolId owner,SourceRange range) { + TypeId id=typeBindings.get().nextTypeId();typeBindings.get().defineType(rolePathType(id,role,scopeRoot(owner),range,false));return id; + } + + TypeId objectPathType(SymbolId target, SourceRange range) { + TypeId type = typeBindings.get().nextTypeId(); + if (inheritance.objectBase(target).isEmpty()) { + typeBindings.get().defineType(new guru.interlis.model.ObjectArgumentType(type, new Origin.DeclaredOrigin(range), + Optional.empty(), List.of(), false)); + } else if (index.byId(target).map(DeclInfo::kind).orElse(null) == SymbolKind.STRUCTURE) { + typeBindings.get().defineType(new StructureValueType(type, new Origin.DeclaredOrigin(range), Optional.of(target), List.of())); + } else { + typeBindings.get().defineType(new ReferenceType(type, new Origin.DeclaredOrigin(range), Optional.of(target), List.of(), false)); + } + return type; + } + + Optional findMember(SymbolId owner, String name) { + SymbolId current = owner; + Set seen = new HashSet<>(); + while (current != null && seen.add(current)) { + work.checkpoint(); + for (DeclInfo child : index.children(current)) { + work.checkpoint(); + if ((child.kind() == SymbolKind.ATTRIBUTE || child.kind() == SymbolKind.ROLE || child.kind() == SymbolKind.VIEW_BASE) + && child.name().equals(name)) { + return Optional.of(child.id()); + } + } + Symbol predefinedOwner = state.symbol(current); + if (predefinedOwner != null && predefinedOwner.origin() instanceof Origin.PredefinedOrigin) { + var members = predefinedOwner instanceof StructureSymbol s ? s.members() : predefinedOwner instanceof ClassSymbol c ? c.members() : List.of(); + for (SymbolId member : members) if (state.symbol(member).kind() == SymbolKind.ATTRIBUTE && state.symbol(member).name().value().equals(name)) return Optional.of(member); + } + var viewMember = views.get().member(current, name); + if (viewMember.isPresent()) return viewMember; + current = inheritance.objectInheritanceBase(current).orElse(null); + } + return Optional.empty(); + } + + SymbolId scopeRoot(SymbolId symbol) { + SymbolId current = symbol; + Set seen = new HashSet<>(); + while (current != null && seen.add(current)) { + work.checkpoint(); + DeclInfo info = index.byId(current).orElse(null); + if (info == null) { + return null; + } + if (info.kind() == SymbolKind.TOPIC || info.kind() == SymbolKind.MODEL) { + return current; + } + current = info.owner().orElse(null); + } + return null; + } + + private record RoleAccess(SymbolId association, RoleSymbol source, Optional target) { } + + private enum PathContext { VALUE, INSPECTION, FUNCTION_PATH } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/OidBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/OidBindings.java new file mode 100644 index 00000000..021ae24c --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/OidBindings.java @@ -0,0 +1,212 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.model.AssociationSymbol; +import guru.interlis.model.Cardinality; +import guru.interlis.model.ClassSymbol; +import guru.interlis.model.DeclaredName; +import guru.interlis.model.Documentation; +import guru.interlis.model.DomainSymbol; +import guru.interlis.model.MetaAttributeSet; +import guru.interlis.model.NumericRange; +import guru.interlis.model.NumericType; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolHeader; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TextKind; +import guru.interlis.model.TextType; +import guru.interlis.model.TopicSymbol; +import guru.interlis.model.TypeId; +import guru.interlis.syntax.AssociationDeclarationSyntax; +import guru.interlis.syntax.ClassDeclarationSyntax; +import guru.interlis.syntax.TopicSyntax; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Binds object identification rules and their predefined domains. */ +final class OidBindings { + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameBindings names; + private final Supplier typeBindings; + private final Supplier inheritance; + private final Consumer defineOidDomain; + private final Consumer replaceOidOwner; + private final Function scopeRoot; + + OidBindings(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameBindings names, + Supplier typeBindings, + Supplier inheritance, + Consumer defineOidDomain, + Consumer replaceOidOwner, + Function scopeRoot) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.names = names; + this.typeBindings = typeBindings; + this.inheritance = inheritance; + this.defineOidDomain = defineOidDomain; + this.replaceOidOwner = replaceOidOwner; + this.scopeRoot = scopeRoot; + } + + void bindOidRules() { + Map> bound = new LinkedHashMap<>(); + for (Symbol symbol : List.copyOf(state.symbolsInBuildOrder())) { + if (symbol instanceof ClassSymbol || symbol instanceof TopicSymbol || symbol instanceof AssociationSymbol) + bindOidRules(symbol.id(), bound, new HashSet<>()); + } + } + + List bindOidRules(SymbolId id, + Map> bound, Set active) { + work.checkpoint(); + if (bound.containsKey(id)) return bound.get(id); + if (!active.add(id)) return List.of(); // Existing inheritance checks diagnose the cycle. + Symbol symbol = state.symbol(id); + DeclInfo decl = index.byId(id).orElse(null); + if (decl == null || !(symbol instanceof ClassSymbol || symbol instanceof TopicSymbol || symbol instanceof AssociationSymbol)) + return List.of(); + List declared = switch (decl.syntax()) { + case ClassDeclarationSyntax c -> c.oidRules(); + case TopicSyntax t -> t.oidRules(); + case AssociationDeclarationSyntax a -> a.oidRules(); + default -> List.of(); + }; + List inherited = state.hasBase(id) + ? bindOidRules(state.baseOf(id), bound, active) : List.of(); + List rules = new ArrayList<>(inherited); + if (!(symbol instanceof TopicSymbol) && rules.isEmpty()) { + SymbolId topic = scopeRoot.apply(id); + if (topic != null && !topic.equals(id) && state.symbol(topic) instanceof TopicSymbol) + bindOidRules(topic, bound, active).stream().filter(r -> !r.basket()).forEach(rules::add); + } + for (var rule : declared) { + work.checkpoint(); + if (symbol instanceof TopicSymbol && rule.domain().isEmpty()) { + diagnostics.emitRule(decl, "NO OID is only allowed on classes and associations", "OID_TOPIC"); + continue; + } + Optional domain = Optional.empty(); + if (rule.domain().isPresent()) { + var name = rule.domain().orElseThrow(); + domain=names.bindTarget(decl,name,Set.of(SymbolKind.DOMAIN),ReferenceUsage.TYPE_USE,new ArrayList<>()); + if (domain.isEmpty()) continue; + if (!(typeBindings.get().effectiveDescriptor(state.declaredType(domain.orElseThrow())) instanceof guru.interlis.model.OidType)) { + diagnostics.emitRule(decl, "OID AS must bind an OID domain", "OID_DOMAIN"); continue; + } + } + var own = new guru.interlis.model.OidRule(rule.basket(), domain, new Origin.DeclaredOrigin(rule.range())); + var previous = inherited.stream().filter(r -> r.basket() == own.basket()).findFirst(); + if (previous.isPresent() && !oidOverrideAllowed(previous.orElseThrow(), own, decl.language(), symbol instanceof TopicSymbol)) + diagnostics.emitRule(decl, "OID rule cannot change its inherited domain", "OID_OVERRIDE"); + rules.removeIf(r -> r.basket() == own.basket()); rules.add(own); + } + // Only 2.4 defines an omitted class rule as inherited NO OID. + if (decl.language() == guru.interlis.model.LanguageVersion.ILI24 + && state.symbol(state.baseOf(id)) instanceof ClassSymbol && inherited.isEmpty() + && rules.stream().anyMatch(r -> !r.basket() && oidState(r) == 2)) + diagnostics.emitRule(decl, "inherited NO OID must first be concretized as ANYOID", "OID_OVERRIDE"); + rules.sort(Comparator.comparing(r -> !r.basket())); + for (var rule : rules) { + if (oidState(rule) == 1 && !decl.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT)) + diagnostics.emitRule(decl, "ANYOID requires an abstract topic, class or association", "OID_ABSTRACT"); + } + if (symbol instanceof AssociationSymbol association) { + Cardinality cardinality = association.cardinality(); + boolean multiple = !(cardinality.upper() instanceof guru.interlis.model.CardinalityUpper.Finite finite) + || finite.value().compareTo(java.math.BigInteger.ONE) > 0; + boolean identified = rules.isEmpty() + ? inheritance.get().baseChain(id).stream().map(index::byId).flatMap(Optional::stream) + .anyMatch(d -> d.modifiers().contains(guru.interlis.model.Modifier.OID)) + : rules.stream().anyMatch(r -> !r.basket() && oidState(r) != 0); + if (multiple && !identified) + diagnostics.emitRule(decl, "multiple links for the same participants require OID", "ASSOCIATION_CARDINALITY_OID"); + } + var h = symbol.header(); + var header = new SymbolHeader(h.id(), h.kind(), h.name(), h.owner(), h.declarationOrder(), h.origin(), + h.nameRange(), h.languageVersion(), h.documentation(), h.metaAttributes(), h.modifiers(), rules); + Symbol replacement = switch (symbol) { + case ClassSymbol c -> new ClassSymbol(header, c.baseType(), c.members(), c.constraints()); + case TopicSymbol t -> new TopicSymbol(header, t.baseTopic(), t.members(), t.dependencies(), t.deferredGenerics(), t.genericBindings()); + case AssociationSymbol a -> new AssociationSymbol(header, a.baseAssociation(), a.cardinality(), a.roles(), a.attributes(), a.constraints(), a.derivedFrom()); + default -> symbol; + }; + replaceOidOwner.accept(replacement); active.remove(id); + var result = List.copyOf(rules); bound.put(id, result); return result; + } + + int oidState(guru.interlis.model.OidRule rule) { + if (rule.domain().isEmpty()) return 0; + Symbol domain = state.symbol(rule.domain().orElseThrow()); + if (domain != null && domain.name().value().equals("INTERLIS.NOOID")) return 0; + return typeBindings.get().effectiveDescriptor(state.declaredType(rule.domain().orElseThrow())) instanceof guru.interlis.model.OidType oid + && oid.oidKind() == guru.interlis.model.OidKind.ANY ? 1 : 2; + } + + private boolean oidOverrideAllowed(guru.interlis.model.OidRule base, guru.interlis.model.OidRule derived, + guru.interlis.model.LanguageVersion language, boolean topic) { + if (base.domain().equals(derived.domain())) return true; + if (base.domain().isPresent() && derived.domain().isPresent()) { + Symbol a = state.symbol(base.domain().orElseThrow()), b = state.symbol(derived.domain().orElseThrow()); + if (a != null && b != null && a.header().origin() instanceof Origin.SyntheticOrigin + && b.header().origin() instanceof Origin.SyntheticOrigin && a.name().equals(b.name())) return true; + } + int from = oidState(base), to = oidState(derived); + if (topic) return false; // A topic assignment is fixed, including ANYOID. + return from == 0 && to == 0 || from == 1 && to == 2 + || language == guru.interlis.model.LanguageVersion.ILI24 && from == 0 && to == 1; + } + + Optional builtinOidDomain(SymbolId context, String name) { + if (!Set.of("INTERLIS.NOOID", "INTERLIS.ANYOID", "INTERLIS.I32OID", "INTERLIS.STANDARDOID", "INTERLIS.UUIDOID").contains(name)) + return Optional.empty(); + var language = index.byId(context).orElseThrow().language(); + if (name.equals("INTERLIS.NOOID") && language == guru.interlis.model.LanguageVersion.ILI23) + return Optional.empty(); + SymbolId symbol = new SymbolId("predefined:" + language + ":domain:" + name); + if (state.hasSymbol(symbol)) return Optional.of(symbol); + Optional base = language == guru.interlis.model.LanguageVersion.ILI23 || name.equals("INTERLIS.NOOID") + ? Optional.empty() + : builtinOidDomain(context, name.equals("INTERLIS.ANYOID") ? "INTERLIS.NOOID" : "INTERLIS.ANYOID"); + Origin origin = new Origin.PredefinedOrigin(language, name); + var kind = name.equals("INTERLIS.I32OID") ? guru.interlis.model.OidKind.NUMERIC + : name.equals("INTERLIS.UUIDOID") ? guru.interlis.model.OidKind.UUID + : name.equals("INTERLIS.STANDARDOID") ? guru.interlis.model.OidKind.TEXT : guru.interlis.model.OidKind.ANY; + Optional carrier = Optional.empty(); + if (kind != guru.interlis.model.OidKind.ANY) { + TypeId value = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(kind == guru.interlis.model.OidKind.NUMERIC + ? new NumericType(value, origin, NumericRange.of(guru.interlis.model.DecimalLiteral.parse("0"), guru.interlis.model.DecimalLiteral.parse("2147483647"))) + : new TextType(value, origin, TextKind.TEXT, java.util.OptionalInt.of(kind == guru.interlis.model.OidKind.UUID ? 36 : 16))); + carrier = Optional.of(value); + } + TypeId type = typeBindings.get().nextTypeId(); typeBindings.get().defineType(new guru.interlis.model.OidType(type, origin, kind, carrier)); + typeBindings.get().bindDeclaredType(symbol, type); base.ifPresent(b -> inheritance.get().bindBase(symbol, b)); + defineOidDomain.accept(new DomainSymbol(new SymbolHeader(symbol, SymbolKind.DOMAIN, DeclaredName.synthetic(name), + Optional.empty(), 0, origin, Optional.empty(), language, + Documentation.empty(), MetaAttributeSet.empty(), language == guru.interlis.model.LanguageVersion.ILI24 && name.equals("INTERLIS.ANYOID") + ? Set.of(guru.interlis.model.Modifier.ABSTRACT) : Set.of(), List.of()), type, base, false)); + return Optional.of(symbol); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/OverlayBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/OverlayBindings.java index 830886ae..91db5758 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/OverlayBindings.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/OverlayBindings.java @@ -125,8 +125,8 @@ private void metadata(DeclInfo target,String key,List values,OverlaySour for(var group:groups){charge();if(group.source().priority()==source.priority()&&!group.values().equals(values))error(source,target.qualifiedName(),"conflicting metadata at equal priority: "+key);} groups.add(new Values(source,values,order++)); } - void validateCrs(GraphBuilder g) { - metadata.forEach((id,keys)->{if(keys.containsKey("CRS")){g.checkWork();if(!(g.effectiveDescriptor(g.declaredTypes.get(id)) instanceof CoordinateType)) + void validateCrs(SemanticGraphRead state, SemanticWork work, TypeBindings types) { + metadata.forEach((id,keys)->{if(keys.containsKey("CRS")){work.checkpoint();if(!(types.effectiveDescriptor(state.declaredType(id)) instanceof CoordinateType)) error(keys.get("CRS").getFirst().source(),index.byId(id).orElseThrow().qualifiedName(),"CRS target is not a coordinate declaration");}}); } List metadata(SymbolId id) { diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/PredefinedModels.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/PredefinedModels.java index 396b8fc9..8e9c3c93 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/PredefinedModels.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/PredefinedModels.java @@ -17,32 +17,36 @@ private PredefinedModels() { } "min","h","d","M","Y","BaseTimeSystems","TimeOfDay","UTC","GregorianYear","GregorianDate","GregorianDateTime","XMLTime","XMLDate","XMLDateTime", "LineSegment","StartSegment","StraightSegment","ArcSegment","SurfaceEdge","SurfaceBoundary","LineGeometry", "areAreasWithoutGaps","areAreasWithoutGaps2"); - static void finish(GraphBuilder graph, Set languages) { + static void finish(SemanticWork work, SemanticGraphRead.Symbols state, Set languages) { // Analysis also publishes declarations outside the requested closure. // Their predefined values need owners even when their language is not // part of the selected schema's language set. Set materialized=new LinkedHashSet<>(languages); - for(var symbol:graph.symbols.values()) { - graph.checkWork(); + for(var symbol:state.symbolsInBuildOrder()) { + work.checkpoint(); if(symbol.origin() instanceof Origin.PredefinedOrigin p)materialized.add(p.language()); } for(var language:materialized) { - graph.checkWork();var model=id(language);var origin=new Origin.PredefinedOrigin(language,"INTERLIS"); - var members=graph.symbols.values().stream().filter(s->s.owner().isEmpty() + work.checkpoint();var model=id(language);var origin=new Origin.PredefinedOrigin(language,"INTERLIS"); + var members=state.symbolsInBuildOrder().stream().filter(s->s.owner().isEmpty() && s.origin() instanceof Origin.PredefinedOrigin p && p.language()==language && p.key().startsWith("INTERLIS.")) .sorted(Comparator.comparingInt((Symbol s)->order(((Origin.PredefinedOrigin)s.origin()).key())) .thenComparing(s->((Origin.PredefinedOrigin)s.origin()).key())).toList(); for(var member:members) { - graph.checkWork();var h=member.header(); + work.checkpoint();var h=member.header(); var owned=new SymbolHeader(h.id(),h.kind(),h.name(),Optional.of(model),order(((Origin.PredefinedOrigin)h.origin()).key()),h.origin(),h.nameRange(), h.languageVersion(),h.documentation(),h.metaAttributes(),h.modifiers(),h.oidRules()); - graph.symbols.put(member.id(),withHeader(member,owned)); + state.replaceSymbol(withHeader(member,owned)); } - graph.symbols.put(model, declaration(language, members.stream().map(Symbol::id).toList())); + var declaration = declaration(language, members.stream().map(Symbol::id).toList()); + if (state.hasSymbol(model)) state.replaceSymbol(declaration); + else state.defineSymbol(declaration); } } - static SymbolId ensure(GraphBuilder graph, LanguageVersion language) { - graph.symbols.putIfAbsent(id(language), declaration(language, List.of())); return id(language); + static SymbolId ensure(SemanticGraphRead.Symbols state, LanguageVersion language) { + SymbolId model = id(language); + if (!state.hasSymbol(model)) state.defineSymbol(declaration(language, List.of())); + return model; } private static ModelSymbol declaration(LanguageVersion language, List members) { var model=id(language); var origin=new Origin.PredefinedOrigin(language,"INTERLIS"); diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/PredefinedTemporal.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/PredefinedTemporal.java index a1786a49..345942b4 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/PredefinedTemporal.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/PredefinedTemporal.java @@ -1,14 +1,34 @@ package guru.interlis.compiler.internal; import guru.interlis.model.*; + import java.math.BigInteger; import java.util.*; +import java.util.function.Supplier; /** The versioned INTERLIS date/time declarations (reference manuals, appendix A). * Built on demand in the owning graph; FORMAT values use the ordinary bound layout. */ final class PredefinedTemporal { - private final GraphBuilder g; - PredefinedTemporal(GraphBuilder graph) { g = graph; } + private final SemanticGraphRead.Symbols state; + private final UnitBindings units; + private final Supplier typeBindings; + private final MetadataBindings metadata; + private final InheritanceBindings inheritance; + private final ObjectPathBindings paths; + + PredefinedTemporal(SemanticGraphRead.Symbols state, + UnitBindings units, + Supplier typeBindings, + MetadataBindings metadata, + InheritanceBindings inheritance, + ObjectPathBindings paths) { + this.state = state; + this.units = units; + this.typeBindings = typeBindings; + this.metadata = metadata; + this.inheritance = inheritance; + this.paths = paths; + } Optional find(DeclInfo context, String qualified, Set kinds) { if (!qualified.startsWith("INTERLIS.")) return Optional.empty(); @@ -22,7 +42,7 @@ Optional find(DeclInfo context, String qualified, Set kind }; if (kind == null || !kinds.contains(kind)) return Optional.empty(); SymbolId id = id(context, name); - if (g.symbols.containsKey(id)) return Optional.of(id); + if (state.hasSymbol(id)) return Optional.of(id); if (kind == SymbolKind.CLASS) timeSystem(context, "UTC"); else if (kind == SymbolKind.UNIT) unit(context, name); else if (kind == SymbolKind.STRUCTURE) structure(context, name); @@ -37,24 +57,24 @@ private SymbolId require(DeclInfo context, String name, SymbolKind kind) { private void unit(DeclInfo context, String name) { String parent = switch (name) { case "min" -> "s"; case "h" -> "min"; case "d" -> "h"; default -> "TIME"; }; var base = parent.equals("min") || parent.equals("h") ? Optional.of(require(context, parent, SymbolKind.UNIT)) - : g.builtinUnit(context.id(), "INTERLIS." + parent); + : units.builtinUnit(context.id(), "INTERLIS." + parent); String factor = switch (name) { case "min", "h" -> "60"; case "d" -> "24"; default -> null; }; var factors = factor == null ? List.of() : List.of( new UnitSymbol.ConversionFactor(UnitSymbol.Operator.MULTIPLY, UnitSymbol.ConstantKind.DECIMAL, Optional.of(DecimalLiteral.parse(factor)), new guru.interlis.source.SourceRange(new guru.interlis.source.SourceId("predefined:/INTERLIS-" + context.language()), guru.interlis.source.SourceRevision.initial(), 0, 0))); var h = header(context, name, SymbolKind.UNIT, Optional.empty(), 0, Set.of()); - g.symbols.put(h.id(), new UnitSymbol(h, switch (name) { case "min" -> "Minute"; case "h" -> "Hour"; + state.defineSymbol(new UnitSymbol(h, switch (name) { case "min" -> "Minute"; case "h" -> "Hour"; case "d" -> "Day"; case "M" -> "Month"; default -> "Year"; }, false, factor == null ? base : Optional.empty(), factor == null ? Optional.empty() : base, List.of(), factors, Optional.empty(), Optional.empty())); } private TypeId number(DeclInfo context, String lo, String hi, String unit, boolean circular, String system) { - var id = g.nextTypeId(); - var boundUnit = unit.equals("s") ? g.builtinUnit(context.id(), "INTERLIS.s") + var id = typeBindings.get().nextTypeId(); + var boundUnit = unit.equals("s") ? units.builtinUnit(context.id(), "INTERLIS.s") : Optional.of(require(context, unit, SymbolKind.UNIT)); var reference = system == null ? Optional.empty() : Optional.of(new NumericRange.ReferenceSystem(timeSystem(context, system), false, OptionalInt.empty())); - g.types.put(id, new NumericType(id, origin(context, "numeric:" + unit), + typeBindings.get().defineType(new NumericType(id, origin(context, "numeric:" + unit), new NumericRange(Optional.of(DecimalLiteral.parse(lo)), Optional.of(DecimalLiteral.parse(hi)), circular, NumericDirection.NONE, boundUnit, reference))); return id; @@ -62,36 +82,36 @@ private TypeId number(DeclInfo context, String lo, String hi, String unit, boole private SymbolId timeSystem(DeclInfo context, String name) { var object = id(context, "BaseTimeSystems." + name); - if (g.symbols.containsKey(object)) return object; + if (state.hasSymbol(object)) return object; var topic = header(context, "TIMESYSTEMS", SymbolKind.TOPIC, Optional.empty(), 0, Set.of()); - var base = g.metadata.predefined(context, "INTERLIS.SCALSYSTEM", Set.of(SymbolKind.CLASS)).orElseThrow(); + var base = metadata.predefined(context, "INTERLIS.SCALSYSTEM", Set.of(SymbolKind.CLASS)).orElseThrow(); var classes = new ArrayList(); for (String className : List.of("CALENDAR", "TIMEOFDAYSYS")) { var h = header(context, "TIMESYSTEMS." + className, SymbolKind.CLASS, Optional.of(topic.id()), classes.size(), Set.of()); - g.baseOf.put(h.id(), base); - var unitType = g.nextTypeId(); - g.types.put(unitType, new NumericType(unitType, h.origin(), new NumericRange(Optional.empty(), Optional.empty(), - false, NumericDirection.NONE, g.builtinUnit(context.id(), "INTERLIS.TIME"), Optional.empty()))); + inheritance.bindBase(h.id(), base); + var unitType = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new NumericType(unitType, h.origin(), new NumericRange(Optional.empty(), Optional.empty(), + false, NumericDirection.NONE, units.builtinUnit(context.id(), "INTERLIS.TIME"), Optional.empty()))); var original = header(context, "TIMESYSTEMS." + className + ".Unit", SymbolKind.PARAMETER, Optional.of(h.id()), 0, Set.of(Modifier.EXTENDED)); var parameter = new SymbolHeader(original.id(), original.kind(), DeclaredName.declared("Unit"), original.owner(), 0, original.origin(), Optional.empty(), context.language(), Documentation.empty(), MetaAttributeSet.empty(), original.modifiers(), List.of()); - var inherited = ((ClassSymbol)g.symbols.get(base)).members().stream() - .filter(id -> g.symbols.get(id).kind() == SymbolKind.PARAMETER).findFirst(); - g.symbols.put(parameter.id(), new ParameterSymbol(parameter, unitType, false, inherited)); - g.declaredTypes.put(parameter.id(), unitType); - g.symbols.put(h.id(), new ClassSymbol(h, Optional.of(base), List.of(parameter.id()), List.of())); - g.effectiveMembers.put(h.id(), g.effectiveMembers.getOrDefault(base, List.of()).stream().map(m -> + var inherited = ((ClassSymbol)state.symbol(base)).members().stream() + .filter(id -> state.symbol(id).kind() == SymbolKind.PARAMETER).findFirst(); + state.defineSymbol(new ParameterSymbol(parameter, unitType, false, inherited)); + typeBindings.get().bindDeclaredType(parameter.id(), unitType); + state.defineSymbol(new ClassSymbol(h, Optional.of(base), List.of(parameter.id()), List.of())); + inheritance.setEffectiveMembers(h.id(), state.effectiveMembers(base).stream().map(m -> new EffectiveMember(m.declaredSymbol(), h.id(), List.of(base, h.id()), m.position(), EffectiveMemberOrigin.INHERITED, false)).toList()); classes.add(h.id()); } - g.symbols.put(topic.id(), new TopicSymbol(topic, Optional.empty(), classes, List.of(), List.of(), List.of())); + state.defineSymbol(new TopicSymbol(topic, Optional.empty(), classes, List.of(), List.of(), List.of())); var basket = header(context, "BaseTimeSystems", SymbolKind.METADATA_BASKET, Optional.empty(), 0, Set.of()); List objects = new ArrayList<>(); for (String objectName : List.of("GregorianCalendar", "UTC")) { var h = header(context, "BaseTimeSystems." + objectName, SymbolKind.METAOBJECT, Optional.of(basket.id()), objects.size(), Set.of()); - g.symbols.put(h.id(), new MetaObjectSymbol(h, Optional.of(classes.get(objects.size())))); objects.add(h.id()); + state.defineSymbol(new MetaObjectSymbol(h, Optional.of(classes.get(objects.size())))); objects.add(h.id()); } - g.symbols.put(basket.id(), new MetadataBasketSymbol(basket, MetadataBasketSymbol.Kind.REFSYSTEM, + state.defineSymbol(new MetadataBasketSymbol(basket, MetadataBasketSymbol.Kind.REFSYSTEM, Optional.of(topic.id()), Optional.empty(), objects)); return object; } @@ -101,16 +121,16 @@ private void structure(DeclInfo context, String name) { var base = parent == null ? Optional.empty() : Optional.of(require(context, parent, SymbolKind.STRUCTURE)); var h = header(context, name, SymbolKind.STRUCTURE, Optional.empty(), 0, name.equals("TimeOfDay") ? Set.of(Modifier.ABSTRACT) : Set.of()); - base.ifPresent(id -> g.baseOf.put(h.id(), id)); + base.ifPresent(id -> inheritance.bindBase(h.id(), id)); List members = new ArrayList<>(); if (name.equals("GregorianDate")) { - var type = g.nextTypeId(); - g.types.put(type, new DomainReferenceType(type, h.origin(), require(context, "GregorianYear", SymbolKind.DOMAIN))); + var type = typeBindings.get().nextTypeId(); + typeBindings.get().defineType(new DomainReferenceType(type, h.origin(), require(context, "GregorianYear", SymbolKind.DOMAIN))); members.add(attribute(context, h, "Year", type, false, false, Optional.empty(), 0)); members.add(attribute(context, h, "Month", number(context, "1", "12", "M", false, null), true, false, Optional.empty(), 1)); members.add(attribute(context, h, "Day", number(context, "1", "31", "d", false, null), true, false, Optional.empty(), 2)); } else { - Optional override = name.equals("UTC") ? g.findMember(base.orElseThrow(), "Hours") : Optional.empty(); + Optional override = name.equals("UTC") ? paths.findMember(base.orElseThrow(), "Hours") : Optional.empty(); members.add(attribute(context, h, "Hours", number(context, "0", "23", "h", true, name.equals("TimeOfDay") ? null : "UTC"), name.equals("GregorianDateTime"), false, override, 0)); if (!name.equals("UTC")) { @@ -118,16 +138,16 @@ private void structure(DeclInfo context, String name) { members.add(attribute(context, h, "Seconds", number(context, "0.000", "59.999", "s", true, null), true, true, Optional.empty(), 2)); } } - g.symbols.put(h.id(), new StructureSymbol(h, base, members, List.of())); + state.defineSymbol(new StructureSymbol(h, base, members, List.of())); List effective = new ArrayList<>(); - base.ifPresent(id -> g.effectiveMembers.getOrDefault(id, List.of()).forEach(m -> { - var replacement = members.stream().filter(member -> ((AttributeSymbol)g.symbols.get(member)).overrides().filter(m.declaredSymbol()::equals).isPresent()).findFirst(); + base.ifPresent(id -> state.effectiveMembers(id).forEach(m -> { + var replacement = members.stream().filter(member -> ((AttributeSymbol)state.symbol(member)).overrides().filter(m.declaredSymbol()::equals).isPresent()).findFirst(); effective.add(new EffectiveMember(replacement.orElse(m.declaredSymbol()), h.id(), List.of(id, h.id()), effective.size(), replacement.isPresent() ? EffectiveMemberOrigin.OVERRIDDEN : EffectiveMemberOrigin.INHERITED, replacement.isPresent())); })); for (var member : members) if (effective.stream().noneMatch(m -> m.declaredSymbol().equals(member))) effective.add(new EffectiveMember(member, h.id(), List.of(h.id()), effective.size(), EffectiveMemberOrigin.LOCAL, false)); - g.effectiveMembers.put(h.id(), List.copyOf(effective)); + inheritance.setEffectiveMembers(h.id(), List.copyOf(effective)); } private SymbolId attribute(DeclInfo context, SymbolHeader owner, String name, TypeId type, boolean subdivision, @@ -136,8 +156,8 @@ private SymbolId attribute(DeclInfo context, SymbolHeader owner, String name, Ty Optional.of(owner.id()), order, overrides.isPresent() ? Set.of(Modifier.EXTENDED) : Set.of()); h = new SymbolHeader(h.id(), h.kind(), DeclaredName.declared(name), h.owner(), order, h.origin(), Optional.empty(), context.language(), Documentation.empty(), MetaAttributeSet.empty(), h.modifiers(), List.of()); - g.symbols.put(h.id(), new AttributeSymbol(h, type, false, false, continuous, subdivision, overrides, List.of())); - g.declaredTypes.put(h.id(), type); return h.id(); + state.defineSymbol(new AttributeSymbol(h, type, false, false, continuous, subdivision, overrides, List.of())); + typeBindings.get().bindDeclaredType(h.id(), type); return h.id(); } private void domain(DeclInfo context, String name) { @@ -156,13 +176,13 @@ private void domain(DeclInfo context, String name) { field(parts, structure, "Minutes", context.language() == LanguageVersion.ILI24 ? 2 : 0, h.origin()); literal(parts, ":", h.origin()); field(parts, structure, "Seconds", context.language() == LanguageVersion.ILI24 ? 2 : 0, h.origin()); } - type = g.nextTypeId(); + type = typeBindings.get().nextTypeId(); var kind = name.equals("XMLDate") ? TemporalKind.DATE : name.equals("XMLTime") ? TemporalKind.TIMEOFDAY : TemporalKind.DATETIME; - g.types.put(type, new FormattedType(type, h.origin(), "INTERLIS." + name, Optional.of(kind), base, + typeBindings.get().defineType(new FormattedType(type, h.origin(), "INTERLIS." + name, Optional.of(kind), base, Optional.of(structure), parts, Optional.empty(), Optional.empty())); } - g.symbols.put(h.id(), new DomainSymbol(h, type, base, false)); g.declaredTypes.put(h.id(), type); - base.ifPresent(id -> g.baseOf.put(h.id(), id)); + state.defineSymbol(new DomainSymbol(h, type, base, false)); typeBindings.get().bindDeclaredType(h.id(), type); + base.ifPresent(id -> inheritance.bindBase(h.id(), id)); } private void dateParts(DeclInfo context, SymbolId structure, List parts) { @@ -172,7 +192,7 @@ private void dateParts(DeclInfo context, SymbolId structure, List parts, SymbolId structure, String name, int width, Origin origin) { - parts.add(new FormattedType.Part(Optional.of(g.findMember(structure, name).orElseThrow()), + parts.add(new FormattedType.Part(Optional.of(paths.findMember(structure, name).orElseThrow()), width == 0 ? Optional.empty() : Optional.of(BigInteger.valueOf(width)), Optional.empty(), "", origin)); } private void literal(List parts, String value, Origin origin) { diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ReferenceBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ReferenceBindings.java new file mode 100644 index 00000000..4e5e50c9 --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ReferenceBindings.java @@ -0,0 +1,213 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.model.BindingResolution; +import guru.interlis.model.DomainReferenceType; +import guru.interlis.model.LanguageVersion; +import guru.interlis.model.ModelSymbol; +import guru.interlis.model.NameReference; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceId; +import guru.interlis.model.ReferenceSegment; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.ScopeId; +import guru.interlis.model.SymbolId; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.UnresolvedReason; +import guru.interlis.source.SourceRange; +import guru.interlis.syntax.AssociationDeclarationSyntax; +import guru.interlis.syntax.AttributeDeclarationSyntax; +import guru.interlis.syntax.ClassDeclarationSyntax; +import guru.interlis.syntax.CollectionTypeSyntax; +import guru.interlis.syntax.DomainDeclarationSyntax; +import guru.interlis.syntax.DomainReferenceSyntax; +import guru.interlis.syntax.NameSyntax; +import guru.interlis.syntax.StructureDeclarationSyntax; +import guru.interlis.syntax.TopicSyntax; +import guru.interlis.syntax.TypeSyntax; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + +/** Registers resolved and unresolved references in their original build order. */ +final class ReferenceBindings { + private static final BindingResolution NAME_NOT_FOUND = + new BindingResolution.UnresolvedTarget(UnresolvedReason.NAME_NOT_FOUND, List.of()); + + private final IndexedDeclarations index; + private final SemanticGraphRead state; + private final SemanticWork work; + private final Consumer storeReference; + private final Function ensurePredefined; + private final Set recordedReferences = new HashSet<>(); + private final Map ambiguousSites = new HashMap<>(); + private int referenceCounter; + + ReferenceBindings(IndexedDeclarations index, SemanticGraphRead state, SemanticWork work, + Consumer storeReference, + Function ensurePredefined) { + this.index = index; + this.state = state; + this.work = work; + this.storeReference = storeReference; + this.ensurePredefined = ensurePredefined; + } + + void complete() { + // Walk the complete declaration tree: attributes and roles also carry + // navigable type/role references, not only model-level declarations. + for (IndexedDeclarations.ModelEntry entry : index.models()) { + work.checkpoint(); + if (state.symbol(entry.id()) instanceof ModelSymbol model) { + for (var imported : model.imports()) { + var name = new NameSyntax( + new guru.interlis.syntax.SyntaxNodeId("import-" + referenceCounter), + imported.range(), List.of(new NameSyntax.NameSegmentSyntax( + imported.importedModel(), imported.range()))); + recordReference(entry.declaration(), name, imported.targetModel().orElse(null), ReferenceUsage.IMPORT); + } + } + referenceTree(entry.id()); + } + } + + private void referenceTree(SymbolId owner) { + for (DeclInfo child : index.children(owner)) { + work.checkpoint(); + referenceFor(child); + referenceTree(child.id()); + } + } + + private void referenceFor(DeclInfo decl) { + if (decl.syntax() instanceof ClassDeclarationSyntax clazz && clazz.baseType().isPresent()) { + completeBaseReference(decl, clazz.baseType().get()); + } else if (decl.syntax() instanceof StructureDeclarationSyntax structure + && structure.baseType().isPresent()) { + completeBaseReference(decl, structure.baseType().get()); + } else if (decl.syntax() instanceof TopicSyntax topic && topic.baseTopic().isPresent()) { + completeBaseReference(decl, topic.baseTopic().get()); + } else if (decl.syntax() instanceof DomainDeclarationSyntax domain + && domain.baseDomain().isPresent()) { + completeBaseReference(decl, domain.baseDomain().get()); + } else if (decl.syntax() instanceof AssociationDeclarationSyntax association + && association.baseAssociation().isPresent()) { + completeBaseReference(decl, association.baseAssociation().orElseThrow()); + } else if (decl.syntax() instanceof AttributeDeclarationSyntax attribute) { + referenceForType(decl, attribute.type()); + } + } + + private void referenceForType(DeclInfo decl, TypeSyntax syntax) { + if (syntax instanceof DomainReferenceSyntax reference) { + TypeDescriptor descriptor = state.type(state.declaredType(decl.id())); + if (descriptor instanceof DomainReferenceType domain) { + recordReference(decl, reference.name(), domain.targetDomain(), + ReferenceUsage.TYPE_USE); + } + } else if (syntax instanceof CollectionTypeSyntax collection) { + referenceForType(decl, collection.elementType()); + } + } + + private void completeBaseReference(DeclInfo owner, NameSyntax name) { + var site = new ReferenceSite(owner.id(), name.range(), ReferenceUsage.BASE_TYPE); + var ambiguity = ambiguousSites.get(site); + if (ambiguity == null) + recordReference(owner, name, state.baseOf(owner.id()), ReferenceUsage.BASE_TYPE); + else + record(owner, name, ambiguity, ReferenceUsage.BASE_TYPE); + } + + /** Topic and association base references are published by the final reference walk. */ + void deferBaseAmbiguity(DeclInfo owner, NameSyntax name, NameResolver.Resolution resolution) { + if (resolution.status() == NameResolver.Status.AMBIGUOUS) + ambiguousSites.putIfAbsent(new ReferenceSite(owner.id(), name.range(), ReferenceUsage.BASE_TYPE), + ambiguous(resolution)); + } + + private record ReferenceSite(SymbolId owner, SourceRange range, ReferenceUsage usage) { } + + private record RecordedReference(ReferenceSite site, BindingResolution resolution) { } + + void recordReference(DeclInfo owner, NameSyntax name, + SymbolId target, ReferenceUsage usage) { + record(owner, name, target == null ? NAME_NOT_FOUND : new BindingResolution.BoundTarget(target), usage); + } + + void recordResolution(DeclInfo owner, NameSyntax name, NameResolver.Resolution resolution, ReferenceUsage usage) { + BindingResolution binding = switch (resolution.status()) { + case BOUND -> new BindingResolution.BoundTarget(resolution.target().orElseThrow()); + case UNRESOLVED -> NAME_NOT_FOUND; + case AMBIGUOUS -> ambiguous(resolution); + }; + record(owner, name, binding, usage); + } + + private static BindingResolution.UnresolvedTarget ambiguous(NameResolver.Resolution resolution) { + return new BindingResolution.UnresolvedTarget(UnresolvedReason.AMBIGUOUS_NAME, resolution.candidates()); + } + + private void record(DeclInfo owner, NameSyntax name, BindingResolution resolution, ReferenceUsage usage) { + var site = new ReferenceSite(owner.id(), name.range(), usage); + if (usage == ReferenceUsage.BASE_TYPE && resolution instanceof BindingResolution.UnresolvedTarget unresolved + && unresolved.reason() == UnresolvedReason.AMBIGUOUS_NAME) + ambiguousSites.putIfAbsent(site, unresolved); + ReferenceId id = reserveReference(site, resolution); + if (id == null) return; + List segments = new ArrayList<>(); + SymbolId current = resolution instanceof BindingResolution.BoundTarget bound ? bound.target() : null; + for (int i = name.segments().size() - 1; i >= 0; i--) { + work.checkpoint(); + var segment = name.segments().get(i); + if (i < name.segments().size() - 1) { + current = referenceOwner(current); + while (current != null && !referenceName(current).equals(segment.text())) { + work.checkpoint(); + current = referenceOwner(current); + } + } + segments.addFirst(new ReferenceSegment(segment.text(), segment.range(), Optional.ofNullable(current))); + } + storeReference.accept(new NameReference(id, new ScopeId(owner.id().value()), usage, + segments, name.range(), resolution)); + } + + private SymbolId referenceOwner(SymbolId symbol) { + if (symbol == null) return null; + var declaration = index.byId(symbol); + if (declaration.isPresent()) return declaration.orElseThrow().owner().orElse(null); + var value = state.symbol(symbol); + if (value == null) return null; + if (value.owner().isPresent()) return value.owner().orElseThrow(); + return value.origin() instanceof Origin.PredefinedOrigin predefined && predefined.key().startsWith("INTERLIS.") + ? ensurePredefined.apply(predefined.language()) : null; + } + + private String referenceName(SymbolId symbol) { + return index.byId(symbol).map(DeclInfo::name).orElseGet(() -> { + String name = state.symbol(symbol).name().value(); return name.substring(name.lastIndexOf('.') + 1); + }); + } + + + void recordEnumReference(SymbolId owner, SourceRange range, List segments, SymbolId target) { + if (target == null) return; + var binding = new BindingResolution.BoundTarget(target); + var id = reserveReference(new ReferenceSite(owner, range, ReferenceUsage.EXPRESSION), binding); + if (id == null) return; + storeReference.accept(new NameReference(id, new ScopeId(owner.value()), ReferenceUsage.EXPRESSION, + segments, range, binding)); + } + + private ReferenceId reserveReference(ReferenceSite site, BindingResolution resolution) { + if (!recordedReferences.add(new RecordedReference(site, resolution))) return null; + return new ReferenceId("ref-" + (++referenceCounter)); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/RequestDiagnosticSelector.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/RequestDiagnosticSelector.java index f26eb173..3e94b0f7 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/RequestDiagnosticSelector.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/RequestDiagnosticSelector.java @@ -7,9 +7,11 @@ import guru.interlis.execution.CancellationToken; import guru.interlis.compiler.CompilationRequest; import guru.interlis.compiler.ModelSelector; +import guru.interlis.source.SourceId; import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -22,20 +24,10 @@ private RequestDiagnosticSelector() { } /** Retains original values/order. Unlocated input/configuration problems are conservative globals. */ static List select(List all, Closure closure, RootSelection roots, - CompilationRequest request, + Set syntaxSources, CancellationToken cancellation) { Set explicit = new HashSet<>(roots.diagnostics()); explicit.addAll(closure.diagnostics()); - var syntaxSources = new HashSet<>(closure.sources()); - // Explicit source requests still exist when parser recovery yields no model. - // Names without a source association must never guess a file from its basename. - for (var selector : request.roots()) { - cancellation.throwIfCancellationRequested(); - switch (selector) { - case ModelSelector.BySource source -> syntaxSources.add(source.source()); - case ModelSelector.ByName name -> name.sourceHint().ifPresent(syntaxSources::add); - } - } var selected = new ArrayList(); for (Diagnostic diagnostic : all) { cancellation.throwIfCancellationRequested(); @@ -51,4 +43,18 @@ static List select(List all, Closure closure, RootSelect } return List.copyOf(selected); } + + /** Keeps closure order followed by explicitly associated sources in request order. */ + static Set syntaxSources(Closure closure, CompilationRequest request, CancellationToken cancellation) { + var sources = new LinkedHashSet<>(closure.sources()); + // An explicit source remains requested even if parser recovery yields no model. + for (var selector : request.roots()) { + cancellation.throwIfCancellationRequested(); + switch (selector) { + case ModelSelector.BySource source -> sources.add(source.source()); + case ModelSelector.ByName name -> name.sourceHint().ifPresent(sources::add); + } + } + return sources; + } } diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SegmentContextualizer.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SegmentContextualizer.java index e2b54c95..44accfda 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SegmentContextualizer.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SegmentContextualizer.java @@ -2,12 +2,20 @@ import guru.interlis.model.*; import java.util.*; +import java.util.function.Function; /** One geometry's substitution of INTERLIS.LineCoord. Canonical declarations and * foreign targets remain unchanged; only dependent structures/domains are copied. * State belongs to one ViewBuilder run, never to a published graph or global cache. */ final class SegmentContextualizer { - private final GraphBuilder g; + private final SemanticGraphRead.Symbols state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final InheritanceBindings inheritance; + private final TypeBindings typeBindings; + private final ExpressionBindings expressionBindings; + private final LineForms lineForms; + private final Function ensureSymbol; private final ViewBuilder views; private final DeclInfo owner; private final String prefix; @@ -18,19 +26,32 @@ final class SegmentContextualizer { private final Map types = new HashMap<>(); private final Map expressions = new HashMap<>(); - SegmentContextualizer(GraphBuilder graph, ViewBuilder views, DeclInfo owner, GeometryType geometry) { - g = graph; this.views = views; this.owner = owner; + SegmentContextualizer(SemanticGraphRead.Symbols state, SemanticWork work, IndexedDeclarations index, + ViewBuilder views, DeclInfo owner, GeometryType geometry, + InheritanceBindings inheritance, TypeBindings typeBindings, + ExpressionBindings expressionBindings, LineForms lineForms, + Function ensureSymbol) { + this.state = state; + this.work = work; + this.index = index; + this.views = views; + this.owner = owner; + this.inheritance = inheritance; + this.typeBindings = typeBindings; + this.expressionBindings = expressionBindings; + this.lineForms = lineForms; + this.ensureSymbol = ensureSymbol; prefix = owner.id().value() + ":inspection:" + geometry.id().value() + ":context:"; - placeholder = g.lines.predefined(owner, "INTERLIS.LineCoord", Set.of(SymbolKind.DOMAIN)).orElseThrow(); + placeholder = lineForms.predefined(owner, "INTERLIS.LineCoord", Set.of(SymbolKind.DOMAIN)).orElseThrow(); SymbolId vertex = geometry.vertexDomain().orElseThrow(); - placeholderType = g.declaredTypes.get(placeholder); - symbols.put(placeholder, vertex); types.put(placeholderType, g.declaredTypes.get(vertex)); + placeholderType = state.declaredType(placeholder); + symbols.put(placeholder, vertex); types.put(placeholderType, state.declaredType(vertex)); List roots = new ArrayList<>(); - roots.add(g.lines.predefined(owner, "INTERLIS.LineSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow()); - roots.add(g.lines.predefined(owner, "INTERLIS.StartSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow()); + roots.add(lineForms.predefined(owner, "INTERLIS.LineSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow()); + roots.add(lineForms.predefined(owner, "INTERLIS.StartSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow()); for (SymbolId form : geometry.lineForms()) { - g.index.byId(form).ifPresent(g::buildSymbol); - if (g.symbols.get(form) instanceof LineFormSymbol line) line.segmentStructure().ifPresent(roots::add); + index.byId(form).ifPresent(ensureSymbol::apply); + if (state.symbol(form) instanceof LineFormSymbol line) line.segmentStructure().ifPresent(roots::add); } roots.forEach(this::discover); // Monotonic closure also handles recursive nested structures without a false @@ -40,28 +61,28 @@ final class SegmentContextualizer { do { changed = false; for (var entry : dependencies.entrySet()) { - g.checkWork(); + work.checkpoint(); if (!affected.contains(entry.getKey()) && entry.getValue().stream().anyMatch(affected::contains)) changed |= affected.add(entry.getKey()); } } while (changed); for (SymbolId id : affected) if (!id.equals(placeholder)) { symbols.put(id, new SymbolId(prefix + id.value())); - if (g.symbols.get(id) instanceof StructureSymbol s) { + if (state.symbol(id) instanceof StructureSymbol s) { s.members().forEach(member -> symbols.put(member, new SymbolId(prefix + member.value()))); s.constraints().forEach(member -> symbols.put(member, new SymbolId(prefix + member.value()))); - } else if (g.symbols.get(id) instanceof DomainSymbol) { - g.index.children(id).stream().filter(d -> d.kind() == SymbolKind.CONSTRAINT) + } else if (state.symbol(id) instanceof DomainSymbol) { + index.children(id).stream().filter(d -> d.kind() == SymbolKind.CONSTRAINT) .forEach(d -> symbols.put(d.id(), new SymbolId(prefix + d.id().value()))); } } // Allocate identities before copying expressions/types with self references. - for (SymbolId id : List.copyOf(symbols.keySet())) if (!id.equals(placeholder)) copySymbol(g.symbols.get(id)); - for (SymbolId id : affected) if (g.symbols.get(id) instanceof StructureSymbol) { - var effective = g.effectiveMembers.getOrDefault(id, List.of()).stream().map(m -> new EffectiveMember( + for (SymbolId id : List.copyOf(symbols.keySet())) if (!id.equals(placeholder)) copySymbol(state.symbol(id)); + for (SymbolId id : affected) if (state.symbol(id) instanceof StructureSymbol) { + var effective = state.effectiveMembers(id).stream().map(m -> new EffectiveMember( symbol(m.declaredSymbol()), symbol(m.effectiveOwner()), m.inheritancePath().stream().map(this::symbol).toList(), m.position(), m.origin(), m.overridden())).toList(); - g.effectiveMembers.put(symbol(id), effective); + inheritance.setEffectiveMembers(symbol(id), effective); views.contextualMembers(symbol(id), effective.stream().map(EffectiveMember::declaredSymbol).toList()); } } @@ -82,22 +103,22 @@ Map contextualSymbols() { } private void discover(SymbolId id) { - g.checkWork(); + work.checkpoint(); if (dependencies.containsKey(id)) return; - g.index.byId(id).ifPresent(g::buildSymbol); - var value = g.symbols.get(id); + index.byId(id).ifPresent(ensureSymbol::apply); + var value = state.symbol(id); if (!(value instanceof StructureSymbol) && !(value instanceof DomainSymbol)) return; Set refs = new LinkedHashSet<>(); dependencies.put(id, refs); if (value instanceof StructureSymbol s) { - g.index.byId(id).ifPresent(g::collectEffective); + index.byId(id).ifPresent(inheritance::collectEffective); s.baseType().ifPresent(refs::add); for (SymbolId member : s.members()) { - g.checkWork(); - typeDependencies(g.declaredTypes.get(member), refs, new HashSet<>()); - if (g.symbols.get(member) instanceof AttributeSymbol a) + work.checkpoint(); + typeDependencies(state.declaredType(member), refs, new HashSet<>()); + if (state.symbol(member) instanceof AttributeSymbol a) a.derivations().forEach(e -> expressionDependencies(e, refs, new HashSet<>())); } - s.constraints().forEach(c -> constraintExpressions(((ConstraintSymbol)g.symbols.get(c)).definition()) + s.constraints().forEach(c -> constraintExpressions(((ConstraintSymbol)state.symbol(c)).definition()) .forEach(e -> expressionDependencies(e, refs, new HashSet<>()))); } else if (value instanceof DomainSymbol d) typeDependencies(d.typeId(), refs, new HashSet<>()); for (SymbolId target : List.copyOf(refs)) discover(target); @@ -106,8 +127,8 @@ private void discover(SymbolId id) { private void typeDependencies(TypeId id, Set refs, Set seen) { if (id == null || !seen.add(id)) return; if (id.equals(placeholderType)) { refs.add(placeholder); return; } - g.checkWork(); - switch (g.types.get(id)) { + work.checkpoint(); + switch (state.type(id)) { case DomainReferenceType t -> refs.add(t.targetDomain()); case StructureValueType t -> { t.target().ifPresent(refs::add); refs.addAll(t.restrictions()); } case CollectionType t -> typeDependencies(t.elementType(), refs, seen); @@ -124,7 +145,7 @@ private void typeDependencies(TypeId id, Set refs, Set seen) { private void expressionDependencies(ExpressionId id, Set refs, Set seen) { if (!seen.add(id)) return; - g.checkWork(); var e = g.expressions.get(id); + work.checkpoint(); var e = state.expression(id); typeDependencies(e.typeId(), refs, new HashSet<>()); if (e instanceof SymbolValueExpression s) refs.add(s.target()); if(e instanceof FunctionCallExpression call) for(var path:call.argumentPaths().values()) { @@ -144,9 +165,9 @@ private SymbolHeader header(Symbol original) { } private void copySymbol(Symbol original) { - g.checkWork(); Symbol result = switch (original) { + work.checkpoint(); Symbol result = switch (original) { case StructureSymbol s -> { - s.baseType().ifPresent(base -> g.baseOf.put(symbol(s.id()), symbol(base))); + s.baseType().ifPresent(base -> inheritance.bindBase(symbol(s.id()), symbol(base))); yield new StructureSymbol(header(s), s.baseType().map(this::symbol), s.members().stream().map(this::symbol).toList(), s.constraints().stream().map(this::symbol).toList()); } @@ -157,18 +178,18 @@ yield new StructureSymbol(header(s), s.baseType().map(this::symbol), case ConstraintSymbol c -> new ConstraintSymbol(header(c), constraint(c.definition()), c.target().map(this::symbol)); default -> throw new IllegalStateException("unexpected segment member: " + original.kind()); }; - g.symbols.put(result.id(), result); - TypeId declared = g.declaredTypes.get(original.id()); - if (declared != null) g.declaredTypes.put(result.id(), type(declared)); + state.defineSymbol(result); + TypeId declared = state.declaredType(original.id()); + if (declared != null) typeBindings.bindDeclaredType(result.id(), type(declared)); } private TypeId type(TypeId original) { if (types.containsKey(original)) return types.get(original); Set refs = new HashSet<>(); typeDependencies(original, refs, new HashSet<>()); if (refs.stream().noneMatch(symbols::containsKey)) return original; - g.checkWork(); TypeId id = g.nextTypeId(); types.put(original, id); - var origin = g.types.get(original).origin(); - TypeDescriptor result = switch (g.types.get(original)) { + work.checkpoint(); TypeId id = typeBindings.nextTypeId(); types.put(original, id); + var origin = state.type(original).origin(); + TypeDescriptor result = switch (state.type(original)) { case DomainReferenceType t -> new DomainReferenceType(id, origin, symbol(t.targetDomain())); case StructureValueType t -> new StructureValueType(id, origin, t.target().map(this::symbol), t.restrictions().stream().map(this::symbol).toList()); case CollectionType t -> new CollectionType(id, origin, t.collectionKind(), t.cardinality(), type(t.elementType())); @@ -183,14 +204,14 @@ private TypeId type(TypeId original) { case AttributePathType.Viewable v -> new AttributePathType.Viewable(symbol(v.target())); case AttributePathType.Argument a -> a; }), t.restrictions().stream().map(this::type).toList()); - default -> throw new IllegalStateException("unhandled segment type: " + g.types.get(original).kind()); + default -> throw new IllegalStateException("unhandled segment type: " + state.type(original).kind()); }; - g.types.put(id, result); return id; + typeBindings.defineType(result); return id; } private ExpressionId expression(ExpressionId original) { if (expressions.containsKey(original)) return expressions.get(original); - g.checkWork(); var e = g.expressions.get(original); + work.checkpoint(); var e = state.expression(original); var id = new ExpressionId(prefix + original.value()); expressions.put(original, id); var type = type(e.typeId()); var state = e.typeState(); var range = e.range(); var children = e.children().stream().map(this::expression).toList(); @@ -208,7 +229,7 @@ private ExpressionId expression(ExpressionId original) { case UndefinedExpression x -> new UndefinedExpression(id, type, state, range, children); case ErrorExpression x -> new ErrorExpression(id, type, state, range, children, x.causes()); }; - g.expressions.put(id, result); return id; + expressionBindings.defineExpression(result); return id; } private PathStep step(PathStep step) { diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticBuildState.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticBuildState.java new file mode 100644 index 00000000..eb418c7c --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticBuildState.java @@ -0,0 +1,206 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.model.ClassValueType; +import guru.interlis.model.ConstantExpression; +import guru.interlis.model.EffectiveMember; +import guru.interlis.model.EffectiveType; +import guru.interlis.model.ExpressionId; +import guru.interlis.model.FunctionCallExpression; +import guru.interlis.model.LanguageVersion; +import guru.interlis.model.LiteralExpression; +import guru.interlis.model.NameReference; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceId; +import guru.interlis.model.SemanticGraph; +import guru.interlis.model.SnapshotId; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolId; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; +import guru.interlis.model.TypeKind; +import guru.interlis.model.TypedExpression; +import guru.interlis.source.SourceId; +import guru.interlis.source.SourceRevision; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** The graph being assembled in a single compilation run. */ +final class SemanticBuildState implements SemanticGraphRead.Symbols, SemanticGraphRead.Types, + SemanticGraphRead.Expressions, SemanticGraphRead.Inheritance { + private final Map symbols = new LinkedHashMap<>(); + private final Map types = new LinkedHashMap<>(); + private final Map expressions = new LinkedHashMap<>(); + private final Map references = new LinkedHashMap<>(); + private final Map declaredTypes = new LinkedHashMap<>(); + private final Map effectiveTypes = new LinkedHashMap<>(); + private final Map> effectiveMembers = new LinkedHashMap<>(); + private final Map bases = new LinkedHashMap<>(); + private int typeCounter; + private int expressionCounter; + private boolean frozen; + + public Symbol symbol(SymbolId id) { return symbols.get(id); } + + public boolean hasSymbol(SymbolId id) { return symbols.containsKey(id); } + + public void defineSymbol(Symbol symbol) { + requireOpen(); + if (symbols.putIfAbsent(symbol.id(), symbol) != null) { + throw new IllegalStateException("Symbol already defined: " + symbol.id()); + } + } + + public void replaceSymbol(Symbol symbol) { + requireOpen(); + Symbol previous = symbols.get(symbol.id()); + if (previous == null) throw new IllegalStateException("Symbol not defined: " + symbol.id()); + boolean attachingPredefined = previous.origin() instanceof Origin.PredefinedOrigin + && previous.owner().isEmpty() && symbol.owner().isPresent(); + if (previous.kind() != symbol.kind() || !previous.name().equals(symbol.name()) + || !previous.origin().equals(symbol.origin()) || !previous.nameRange().equals(symbol.nameRange()) + || previous.languageVersion() != symbol.languageVersion() + || (!attachingPredefined && (!previous.owner().equals(symbol.owner()) + || previous.declarationOrder() != symbol.declarationOrder()))) { + throw new IllegalStateException("Replacement changes symbol identity: " + symbol.id()); + } + symbols.replace(symbol.id(), symbol); + } + + /** Read-only live view in insertion order. Take a snapshot before adding symbols while iterating. */ + public Collection symbolsInBuildOrder() { return Collections.unmodifiableCollection(symbols.values()); } + + public TypeDescriptor type(TypeId id) { return types.get(id); } + + public void defineType(TypeDescriptor type) { + requireOpen(); + if (types.putIfAbsent(type.id(), type) != null) + throw new IllegalStateException("Type already defined: " + type.id()); + } + + public void replaceType(TypeDescriptor type) { + requireOpen(); + TypeDescriptor previous = types.get(type.id()); + if (previous == null) throw new IllegalStateException("Type not defined: " + type.id()); + boolean completion = switch (previous.kind()) { + case PENDING -> type.kind() != TypeKind.PENDING; + case NUMERIC, COORDINATE -> type.kind() == previous.kind() || type.kind() == TypeKind.ERROR; + case ENUMERATION, GEOMETRY -> type.kind() == previous.kind(); + default -> false; + }; + // The builtin call specializes its unrestricted result at the call site. + boolean contextualClass = previous instanceof ClassValueType generic && generic.structureValue() + && generic.restrictions().isEmpty() && type instanceof ClassValueType + && generic.origin() instanceof Origin.PredefinedOrigin predefined + && predefined.key().equals(BuiltinFunctions.Value.CLASS_VALUE.name()) + && type.origin() instanceof Origin.DeclaredOrigin; + if (!contextualClass && (!completion || !previous.origin().equals(type.origin()))) + throw new IllegalStateException("Invalid type completion: " + type.id()); + types.replace(type.id(), type); + } + + public TypedExpression expression(ExpressionId id) { return expressions.get(id); } + + public void defineExpression(TypedExpression expression) { + requireOpen(); + if (expressions.putIfAbsent(expression.id(), expression) != null) + throw new IllegalStateException("Expression already defined: " + expression.id()); + } + + public void replaceExpression(TypedExpression expression) { + requireOpen(); + TypedExpression previous = expressions.get(expression.id()); + if (previous == null) throw new IllegalStateException("Expression not defined: " + expression.id()); + if (!(previous instanceof LiteralExpression || previous instanceof ConstantExpression + || previous instanceof FunctionCallExpression) || previous.getClass() != expression.getClass() + || !previous.range().equals(expression.range()) || !previous.children().equals(expression.children())) { + throw new IllegalStateException("Replacement changes expression identity: " + expression.id()); + } + expressions.replace(expression.id(), expression); + } + + void storeReference(NameReference reference) { + requireOpen(); + if (references.putIfAbsent(reference.id(), reference) != null) + throw new IllegalStateException("Reference already defined: " + reference.id()); + } + + public TypeId declaredType(SymbolId id) { return declaredTypes.get(id); } + + public boolean hasDeclaredType(SymbolId id) { return declaredTypes.containsKey(id); } + + + public void bindDeclaredType(SymbolId symbol, TypeId type) { + requireOpen(); + if (declaredTypes.putIfAbsent(symbol, type) != null) + throw new IllegalStateException("Declared type already bound: " + symbol); + } + + /** Read-only live view in insertion order. */ + public Set> declaredTypesInBuildOrder() { + return Collections.unmodifiableMap(declaredTypes).entrySet(); + } + + public void setEffectiveType(SymbolId symbol, EffectiveType type) { + requireOpen(); + if (effectiveTypes.putIfAbsent(symbol, type) != null) + throw new IllegalStateException("Effective type already defined: " + symbol); + } + + /** Returns the members in effective order, or an empty list if none have been recorded. */ + public List effectiveMembers(SymbolId owner) { + return effectiveMembers.getOrDefault(owner, List.of()); + } + + public void setEffectiveMembers(SymbolId owner, List members) { + requireOpen(); + if (effectiveMembers.putIfAbsent(owner, List.copyOf(members)) != null) + throw new IllegalStateException("Effective members already defined: " + owner); + } + + /** Read-only live view in insertion order, with immutable member lists. */ + public Set>> effectiveMembersInBuildOrder() { + return Collections.unmodifiableMap(effectiveMembers).entrySet(); + } + + public SymbolId baseOf(SymbolId symbol) { return bases.get(symbol); } + + public boolean hasBase(SymbolId symbol) { return bases.containsKey(symbol); } + + public void bindBase(SymbolId symbol, SymbolId base) { + requireOpen(); + SymbolId previous = bases.putIfAbsent(symbol, base); + if (previous != null && !previous.equals(base)) + throw new IllegalStateException("Conflicting base for symbol: " + symbol); + } + + public TypeId nextTypeId() { return new TypeId("type-" + nextTypeSequence()); } + + public int nextTypeSequence() { + requireOpen(); + return ++typeCounter; + } + + public ExpressionId nextExpressionId() { + requireOpen(); + return new ExpressionId("expr-" + (++expressionCounter)); + } + + /** Publishes a defensive graph and closes this state to further writes. */ + SemanticGraph freeze(SnapshotId snapshot, List roots, List models, + Set languages, Map revisions) { + requireOpen(); + SemanticGraph graph = new SemanticGraph(snapshot, symbols.values(), types.values(), expressions.values(), + references.values(), declaredTypes, effectiveTypes, effectiveMembers, roots, models, languages, revisions); + frozen = true; + return graph; + } + + private void requireOpen() { + if (frozen) throw new IllegalStateException("Semantic graph already frozen"); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticDiagnostics.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticDiagnostics.java new file mode 100644 index 00000000..7eb14bac --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticDiagnostics.java @@ -0,0 +1,133 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.diagnostics.Diagnostic; +import guru.interlis.diagnostics.DiagnosticArgument; +import guru.interlis.diagnostics.DiagnosticCode; +import guru.interlis.diagnostics.DiagnosticDraft; +import guru.interlis.diagnostics.DiagnosticId; +import guru.interlis.diagnostics.Phase; +import guru.interlis.diagnostics.RelatedLocation; +import guru.interlis.diagnostics.Severity; +import guru.interlis.model.Origin; +import guru.interlis.source.SourceId; +import guru.interlis.source.SourceRange; +import guru.interlis.syntax.NameSyntax; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** Creates and records semantic drafts in the compilation run’s existing collector. */ +final class SemanticDiagnostics { + record UnsupportedFeature(SourceId source, String feature) { } + + private final DiagnosticCollector collector; + private final Closure closure; + private final List syntaxErrors; + private final Map unsupportedFeatures = new LinkedHashMap<>(); + private final Set fragmentTypeFailures = new HashSet<>(); + + SemanticDiagnostics(DiagnosticCollector collector, Closure closure) { + this.collector = Objects.requireNonNull(collector, "collector"); + this.closure = closure; + syntaxErrors = collector.snapshot().stream().filter(d -> d.severity() == Severity.ERROR + && (d.phase() == Phase.LEXING || d.phase() == Phase.PARSING)).toList(); + } + + List syntaxErrors() { return syntaxErrors; } + + List snapshot() { return collector.snapshot(); } + + void importTypeFragmentDiagnostics(List diagnostics) { + var ids = collector.importAll(diagnostics); + for (var diagnostic : diagnostics) { + if (diagnostic.severity() == Severity.ERROR) fragmentTypeFailures.add(ids.get(diagnostic.id())); + } + } + + Set fragmentTypeFailures() { + return Set.copyOf(fragmentTypeFailures); + } + + DiagnosticId emit(DiagnosticDraft draft) { return collector.emit(draft); } + + Map unsupportedFeatures() { + return Map.copyOf(unsupportedFeatures); + } + + DiagnosticId unresolved(NameSyntax name) { + var draft = DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0001"), + Severity.ERROR, Phase.BINDING, "diagnostic.semantics.unresolved") + .at(name.range()) + .argument(new DiagnosticArgument.IdentifierArgument("name", name.text())); + // Only qualified references to an actually failed import are dependent. + // An unrelated unresolved name in the same file remains independently visible. + if (name.segments().size() > 1) { + String qualifier = name.segments().getFirst().text(); + closure.models().stream().filter(m -> m.sourceId().equals(name.range().source()) + && m.syntax().range().startOffset() <= name.range().startOffset() + && m.syntax().range().endOffset() >= name.range().endOffset()) + .flatMap(m -> m.syntax().imports().stream()).filter(i -> i.modelName().equals(qualifier)) + .map(i -> collector.importFailure(i.range())).flatMap(Optional::stream).findFirst() + .ifPresent(id -> { draft.cause(id); draft.dependsOn(id); }); + } + return collector.emit(draft); + } + + DiagnosticId emitUnsupported(SourceId source, SourceRange range, String feature, + String construct) { + DiagnosticDraft draft = DiagnosticDraft.of(new DiagnosticCode("ILI-IMPL-0001"), + Severity.ERROR, Phase.SEMANTIC_CHECKING, + "diagnostic.implementation.unsupported") + .argument(new DiagnosticArgument.TextArgument("feature", feature)) + .argument(new DiagnosticArgument.TextArgument("construct", + construct == null ? feature : construct)); + if (range != null) { + draft.at(range); + } + DiagnosticId id = collector.emit(draft); + unsupportedFeatures.putIfAbsent(id, new UnsupportedFeature(source, feature)); + return id; + } + + DiagnosticId emitTypeMismatch(SourceId source, SourceRange range, String actual, String expected) { + return collector.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0005"), Severity.ERROR, + Phase.TYPE_CHECKING, "diagnostic.semantics.type") + .at(range) + .argument(new DiagnosticArgument.TypeNameArgument("actual", actual)) + .argument(new DiagnosticArgument.TypeNameArgument("expected", expected))); + } + + DiagnosticId emitRule(DeclInfo decl, String rule, String reason) { + return emitRule(decl, rule, reason, ruleRange(decl)); + } + + private SourceRange ruleRange(DeclInfo decl) { + return decl.origin() instanceof Origin.DeclaredOrigin declared + ? declared.range() : decl.nameRange().orElse(null); + } + + DiagnosticId emitRule(DeclInfo decl, String rule, String reason, SourceRange range) { + return emitRule(decl, rule, reason, range, List.of()); + } + + DiagnosticId emitRule(DeclInfo decl, String rule, String reason, List related) { + return emitRule(decl, rule, reason, ruleRange(decl), related); + } + + private DiagnosticId emitRule(DeclInfo decl, String rule, String reason, SourceRange range, + List related) { + var draft = DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0010"), + Severity.ERROR, Phase.SEMANTIC_CHECKING, + "diagnostic.semantics.inheritance") + .at(range) + .argument(new DiagnosticArgument.IdentifierArgument("element", decl.name())) + .argument(new DiagnosticArgument.TextArgument("rule", rule + "/" + reason)); + related.forEach(draft::related); + return collector.emit(draft); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticGraphRead.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticGraphRead.java new file mode 100644 index 00000000..e1cfaf8a --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticGraphRead.java @@ -0,0 +1,62 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.model.EffectiveMember; +import guru.interlis.model.EffectiveType; +import guru.interlis.model.ExpressionId; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolId; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; +import guru.interlis.model.TypedExpression; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Read operations over the current graph as it is assembled in one run. + * Missing individual values return null. Collection views are read-only and preserve build order. + */ +interface SemanticGraphRead { + Symbol symbol(SymbolId id); + boolean hasSymbol(SymbolId id); + TypeDescriptor type(TypeId id); + TypedExpression expression(ExpressionId id); + TypeId declaredType(SymbolId id); + boolean hasDeclaredType(SymbolId id); + SymbolId baseOf(SymbolId id); + boolean hasBase(SymbolId id); + Collection symbolsInBuildOrder(); + Set> declaredTypesInBuildOrder(); + List effectiveMembers(SymbolId owner); + Set>> effectiveMembersInBuildOrder(); + + /** The portion of build state responsible for defining and completing symbols. */ + interface Symbols extends SemanticGraphRead { + void defineSymbol(Symbol symbol); + void replaceSymbol(Symbol symbol); + } + + /** Type descriptors, their allocation sequence, and declaration assignments. */ + interface Types extends SemanticGraphRead { + void defineType(TypeDescriptor type); + void replaceType(TypeDescriptor type); + TypeId nextTypeId(); + int nextTypeSequence(); + void bindDeclaredType(SymbolId symbol, TypeId type); + void setEffectiveType(SymbolId symbol, EffectiveType type); + } + + /** Bound expressions and their allocation sequence. */ + interface Expressions extends SemanticGraphRead { + void defineExpression(TypedExpression expression); + void replaceExpression(TypedExpression expression); + ExpressionId nextExpressionId(); + } + + /** Canonical base edges and effective member lists. */ + interface Inheritance extends SemanticGraphRead { + void bindBase(SymbolId symbol, SymbolId base); + void setEffectiveMembers(SymbolId owner, List members); + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticWork.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticWork.java new file mode 100644 index 00000000..9d41177e --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/SemanticWork.java @@ -0,0 +1,25 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.execution.CancellationToken; + +import java.util.Objects; + +/** A compilation run's semantic work budget, including work already charged by overlays. */ +final class SemanticWork { + private final CancellationToken cancellation; + private final long maximumWork; + private long work; + + SemanticWork(CancellationToken cancellation, long maximumWork, long overlayWork) { + this.cancellation = Objects.requireNonNull(cancellation, "cancellation"); + this.maximumWork = maximumWork; + this.work = overlayWork; + } + + void checkpoint() { + cancellation.throwIfCancellationRequested(); + if (++work > maximumWork) { + throw new AnalysisBudgetExceededException("maxSemanticWorkUnits", maximumWork); + } + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/StructureBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/StructureBindings.java index 81e31c0d..017d06b0 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/StructureBindings.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/StructureBindings.java @@ -3,78 +3,108 @@ import guru.interlis.diagnostics.DiagnosticId; import guru.interlis.model.*; import guru.interlis.syntax.StructureValueTypeSyntax; + import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; /** Structure binding and availability within one graph build; owns no parallel state. */ final class StructureBindings { - private final GraphBuilder g; - StructureBindings(GraphBuilder graph) { g=graph; } + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameBindings names; + private final InheritanceBindings inheritance; + private final Supplier paths; + private final GenericBindings generics; + private final Function effectiveDescriptor; + + StructureBindings(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameBindings names, + InheritanceBindings inheritance, + Supplier paths, + GenericBindings generics, + Function effectiveDescriptor) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.names = names; + this.inheritance = inheritance; + this.paths = paths; + this.generics = generics; + this.effectiveDescriptor = effectiveDescriptor; + } TypeDescriptor bind(TypeId id, StructureValueTypeSyntax syntax, DeclInfo declaration) { List errors=new ArrayList<>(); - Optional target=syntax.target().flatMap(name -> g.bindTarget(declaration,name, + Optional target=syntax.target().flatMap(name -> names.bindTarget(declaration,name, Set.of(SymbolKind.STRUCTURE),ReferenceUsage.TYPE_USE,errors)); List restrictions=new ArrayList<>(); for(var name:syntax.restrictions()) { - g.checkWork();g.bindTarget(declaration,name,Set.of(SymbolKind.STRUCTURE),ReferenceUsage.TYPE_USE,errors).ifPresent(restrictions::add); + work.checkpoint();names.bindTarget(declaration,name,Set.of(SymbolKind.STRUCTURE),ReferenceUsage.TYPE_USE,errors).ifPresent(restrictions::add); } if(!errors.isEmpty())return new ErrorType(id,new Origin.DeclaredOrigin(syntax.range()),errors); - target.ifPresent(g::objectInheritanceBase); + target.ifPresent(inheritance::objectInheritanceBase); for(SymbolId restriction:restrictions) { - g.objectInheritanceBase(restriction); - if(target.isPresent()&&!g.targetSpecializes(restriction,target.orElseThrow())) - errors.add(g.emitRule(declaration,"structure restriction must specialize the primary structure","STRUCTURE_RESTRICTION",syntax.range())); + inheritance.objectInheritanceBase(restriction); + if(target.isPresent()&&!inheritance.targetSpecializes(restriction,target.orElseThrow())) + errors.add(diagnostics.emitRule(declaration,"structure restriction must specialize the primary structure","STRUCTURE_RESTRICTION",syntax.range())); } return errors.isEmpty()?new StructureValueType(id,new Origin.DeclaredOrigin(syntax.range()),target,restrictions) :new ErrorType(id,new Origin.DeclaredOrigin(syntax.range()),errors); } SymbolId target(TypeDescriptor type) { - if(type instanceof CollectionType collection)type=g.effectiveDescriptor(collection.elementType()); - return type instanceof StructureValueType structure?g.commonObjectScope(structure.targetRoots()):null; + if(type instanceof CollectionType collection)type=effectiveDescriptor.apply(collection.elementType()); + return type instanceof StructureValueType structure?paths.get().commonObjectScope(structure.targetRoots()):null; } void validate() { - for(Symbol symbol:g.symbols.values()) { - g.checkWork(); + for(Symbol symbol:state.symbolsInBuildOrder()) { + work.checkpoint(); if(!(symbol instanceof AttributeSymbol attribute))continue; - DeclInfo declaration=g.index.byId(attribute.id()).orElse(null); + DeclInfo declaration=index.byId(attribute.id()).orElse(null); if(declaration==null)continue; - TypeDescriptor type=g.effectiveDescriptor(attribute.declaredType()); + TypeDescriptor type=effectiveDescriptor.apply(attribute.declaredType()); boolean required=attribute.mandatory(); if(type instanceof CollectionType collection) { - required=collection.cardinality().lower().signum()>0;type=g.effectiveDescriptor(collection.elementType()); + required=collection.cardinality().lower().signum()>0;type=effectiveDescriptor.apply(collection.elementType()); } if(required && type instanceof StructureValueType structure && !attribute.header().has(Modifier.ABSTRACT) && !hasConcreteStructure(structure,declaration)) - g.emitRule(declaration,"required structure attribute needs a concrete allowed structure or ABSTRACT","STRUCTURE_ABSTRACT_REQUIRED"); + diagnostics.emitRule(declaration,"required structure attribute needs a concrete allowed structure or ABSTRACT","STRUCTURE_ABSTRACT_REQUIRED"); } - for(Symbol owner:g.symbols.values()) { - g.checkWork(); + for(Symbol owner:state.symbolsInBuildOrder()) { + work.checkpoint(); if(!(owner instanceof ClassSymbol || owner instanceof StructureSymbol || owner instanceof AssociationSymbol) || owner.header().has(Modifier.ABSTRACT))continue; - DeclInfo declaration=g.index.byId(owner.id()).orElse(null); + DeclInfo declaration=index.byId(owner.id()).orElse(null); if(declaration==null)continue; - for(var member:g.effectiveMembers.getOrDefault(owner.id(),List.of())) - if(g.symbols.get(member.declaredSymbol()) instanceof AttributeSymbol attribute && attribute.header().has(Modifier.ABSTRACT)) - g.emitRule(declaration,"concrete viewable retains an abstract attribute","ABSTRACT_MEMBER"); + for(var member:state.effectiveMembers(owner.id())) + if(state.symbol(member.declaredSymbol()) instanceof AttributeSymbol attribute && attribute.header().has(Modifier.ABSTRACT)) + diagnostics.emitRule(declaration,"concrete viewable retains an abstract attribute","ABSTRACT_MEMBER"); } } private boolean hasConcreteStructure(StructureValueType type, DeclInfo declaration) { if(type.targetRoots().isEmpty())return false; for(SymbolId root:type.targetRoots()) - if(g.symbols.get(root) instanceof StructureSymbol structure && !structure.header().has(Modifier.ABSTRACT))return true; - var visible=g.importedModels(g.index.modelOfSymbol(declaration.id()).orElseThrow()); - SymbolId topic=g.scopeRoot(declaration.id()); - for(Symbol symbol:g.symbols.values()) { - g.checkWork(); + if(state.symbol(root) instanceof StructureSymbol structure && !structure.header().has(Modifier.ABSTRACT))return true; + var visible=generics.importedModels(index.modelOfSymbol(declaration.id()).orElseThrow()); + SymbolId topic=paths.get().scopeRoot(declaration.id()); + for(Symbol symbol:state.symbolsInBuildOrder()) { + work.checkpoint(); if(!(symbol instanceof StructureSymbol structure) || structure.header().has(Modifier.ABSTRACT))continue; if(!(structure.origin() instanceof Origin.PredefinedOrigin) - && !visible.contains(g.index.modelOfSymbol(structure.id()).orElse(null)))continue; - SymbolId candidateTopic=g.scopeRoot(structure.id()); - boolean available=!(g.symbols.get(candidateTopic) instanceof TopicSymbol) || Objects.equals(candidateTopic,topic) - || type.targetRoots().stream().anyMatch(root -> Objects.equals(g.scopeRoot(root),candidateTopic)); - if(available && type.targetRoots().stream().anyMatch(root -> g.targetSpecializes(structure.id(),root)))return true; + && !visible.contains(index.modelOfSymbol(structure.id()).orElse(null)))continue; + SymbolId candidateTopic=paths.get().scopeRoot(structure.id()); + boolean available=!(state.symbol(candidateTopic) instanceof TopicSymbol) || Objects.equals(candidateTopic,topic) + || type.targetRoots().stream().anyMatch(root -> Objects.equals(paths.get().scopeRoot(root),candidateTopic)); + if(available && type.targetRoots().stream().anyMatch(root -> inheritance.targetSpecializes(structure.id(),root)))return true; } return false; } diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/TopicBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/TopicBindings.java new file mode 100644 index 00000000..8e10a980 --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/TopicBindings.java @@ -0,0 +1,191 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.model.AssociationSymbol; +import guru.interlis.model.AttributeSymbol; +import guru.interlis.model.ClassSymbol; +import guru.interlis.model.CollectionType; +import guru.interlis.model.EffectiveMember; +import guru.interlis.model.ReferenceType; +import guru.interlis.model.RoleSymbol; +import guru.interlis.model.StructureValueType; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolId; +import guru.interlis.model.TopicSymbol; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Checks topic dependencies and bound reference scopes. */ +final class TopicBindings { + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final Supplier inheritance; + private final Supplier paths; + private final OidBindings oid; + private final Function effectiveDescriptor; + + TopicBindings(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + Supplier inheritance, + Supplier paths, + OidBindings oid, + Function effectiveDescriptor) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.inheritance = inheritance; + this.paths = paths; + this.oid = oid; + this.effectiveDescriptor = effectiveDescriptor; + } + + List topicDependencies(SymbolId topic) { + Set result = new LinkedHashSet<>(); + for (SymbolId id : inheritance.get().baseChain(topic)) { + work.checkpoint(); + if (state.symbol(id) instanceof TopicSymbol owner) result.addAll(owner.dependencies()); + } + return List.copyOf(result); + } + + void validateTopicDependencies() { + for (Symbol symbol : state.symbolsInBuildOrder()) { + if (!(symbol instanceof TopicSymbol topic)) continue; + work.checkpoint(); + Set seen = new HashSet<>(); + var pending = new java.util.ArrayDeque<>(topicDependencies(topic.id())); + while (!pending.isEmpty()) { + work.checkpoint(); + SymbolId next = pending.removeFirst(); + if (next.equals(topic.id())) { + diagnostics.emitRule(index.byId(topic.id()).orElseThrow(), "cyclic topic dependency", "TOPIC_DEPENDENCY_CYCLE"); + break; + } + if (seen.add(next)) pending.addAll(topicDependencies(next)); + } + } + } + + private void validateReferenceScope(DeclInfo site, SymbolId sourceTopic, SymbolId target, + boolean external, String externalRule) { + SymbolId targetTopic = paths.get().scopeRoot(target); + if (!(state.symbol(sourceTopic) instanceof TopicSymbol) + || !(state.symbol(targetTopic) instanceof TopicSymbol) + || isTopicSubtype(sourceTopic, targetTopic)) return; + if (!external) diagnostics.emitRule(site, "cross-topic reference requires EXTERNAL", externalRule); + if (!topicDependencies(sourceTopic).contains(targetTopic)) + diagnostics.emitRule(site, "reference target topic requires DEPENDS ON", "REFERENCE_DEPENDS_ON"); + } + + boolean shareBaseTopic(SymbolId first, SymbolId second) { + Set firstBases = new LinkedHashSet<>(); + SymbolId current = first; + int guard = 0; + while (current != null && guard++ < 32) { + work.checkpoint(); + firstBases.add(current); + current = state.symbol(current) instanceof TopicSymbol topic + ? topic.baseTopic().orElse(null) : null; + } + current = second; + guard = 0; + while (current != null && guard++ < 32) { + work.checkpoint(); + if (firstBases.contains(current)) { + return true; + } + current = state.symbol(current) instanceof TopicSymbol topic + ? topic.baseTopic().orElse(null) : null; + } + return false; + } + + boolean isTopicSubtype(SymbolId candidate, SymbolId base) { + SymbolId current = candidate; + Set visited = new LinkedHashSet<>(); + while (current != null && visited.add(current)) { + work.checkpoint(); + if (current.equals(base)) { + return true; + } + Symbol symbol = state.symbol(current); + if (symbol instanceof TopicSymbol topic) { + current = topic.baseTopic().orElse(null); + } else { + return false; + } + } + return false; + } + + void validateReferenceTargets() { + for (var entry : state.effectiveMembersInBuildOrder()) { + work.checkpoint(); + SymbolId sourceOwner = entry.getKey(); + while (sourceOwner != null && index.byId(sourceOwner).isEmpty()) { + work.checkpoint(); + sourceOwner = state.symbol(sourceOwner).owner().orElse(null); + } + if (sourceOwner == null) continue; // Predefined declarations are checked at their source uses. + DeclInfo owner = index.byId(sourceOwner).orElseThrow(); + SymbolId topic = paths.get().scopeRoot(owner.id()); + Set seen = new HashSet<>(); + var pending = new java.util.ArrayDeque(); + pending.add(entry.getKey()); + while (!pending.isEmpty()) { + work.checkpoint(); + SymbolId container = pending.removeFirst(); + if (!seen.add(container)) continue; + for (EffectiveMember member : state.effectiveMembers(container)) { + work.checkpoint(); + Symbol declaration = state.symbol(member.declaredSymbol()); + List roots = List.of(); + boolean external = false; + if (declaration instanceof RoleSymbol role) { + roots = role.targetRoots(); external = role.external(); + } + else if (declaration instanceof AttributeSymbol attribute) { + TypeDescriptor type = effectiveDescriptor.apply(attribute.declaredType()); + while (type instanceof CollectionType collection) { + work.checkpoint(); type = effectiveDescriptor.apply(collection.elementType()); + } + if (type instanceof StructureValueType structure) pending.addAll(structure.targetRoots()); + if (type instanceof ReferenceType reference) { + roots = reference.targetRoots(); external = reference.external(); + } + } + DeclInfo site = declaration != null && declaration.header().owner().filter(owner.id()::equals).isPresent() + ? index.byId(declaration.id()).orElse(owner) : owner; + for (SymbolId root : roots) { + work.checkpoint(); + Symbol target = state.symbol(paths.get().contextualObject(root, topic)); + if (target == null || index.byId(target.id()).isEmpty()) continue; // ANYCLASS/unresolved: no invented OID. + validateReferenceScope(site, topic, target.id(), external, + declaration instanceof RoleSymbol ? "ROLE_EXTERNAL" : "REFERENCE_EXTERNAL"); + boolean objectOid = target.header().oidRules().stream().anyMatch(r -> !r.basket() && oid.oidState(r) != 0); + if (target instanceof ClassSymbol && owner.language() == guru.interlis.model.LanguageVersion.ILI24 && !objectOid) + diagnostics.emitRule(site, "reference target " + target.name().value() + " has NO OID in INTERLIS 2.4", "REFERENCE_TARGET_NO_OID"); + if (target instanceof AssociationSymbol) { + boolean identified = target.header().oidRules().isEmpty() + ? inheritance.get().baseChain(target.id()).stream().map(index::byId).flatMap(Optional::stream) + .anyMatch(d -> d.modifiers().contains(guru.interlis.model.Modifier.OID)) : objectOid; + if (!identified) diagnostics.emitRule(site, "association target " + target.name().value() + " requires its own OID", "REFERENCE_ASSOCIATION_OID"); + } + } + } + } + } + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/TranslationBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/TranslationBindings.java new file mode 100644 index 00000000..c3cc9466 --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/TranslationBindings.java @@ -0,0 +1,971 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.model.AlignmentType; +import guru.interlis.model.AssociationSymbol; +import guru.interlis.model.AttributeSymbol; +import guru.interlis.model.BlackboxType; +import guru.interlis.model.BooleanType; +import guru.interlis.model.ClassSymbol; +import guru.interlis.model.ClassValueType; +import guru.interlis.model.CollectionType; +import guru.interlis.model.ConstraintDefinition; +import guru.interlis.model.ConstraintSymbol; +import guru.interlis.model.CoordinateType; +import guru.interlis.model.DomainConstraint; +import guru.interlis.model.DomainReferenceType; +import guru.interlis.model.DomainSymbol; +import guru.interlis.model.EnumerationType; +import guru.interlis.model.EnumerationValue; +import guru.interlis.model.ExpressionId; +import guru.interlis.model.FormattedType; +import guru.interlis.model.FunctionSymbol; +import guru.interlis.model.GeometryType; +import guru.interlis.model.LanguageVersion; +import guru.interlis.model.MandatoryConstraint; +import guru.interlis.model.MetaAttributeSet; +import guru.interlis.model.ModelSymbol; +import guru.interlis.model.NumericRange; +import guru.interlis.model.NumericType; +import guru.interlis.model.Origin; +import guru.interlis.model.PlausibilityConstraint; +import guru.interlis.model.ReferenceType; +import guru.interlis.model.RoleSymbol; +import guru.interlis.model.StructureSymbol; +import guru.interlis.model.StructureValueType; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TextType; +import guru.interlis.model.TopicSymbol; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; +import guru.interlis.model.TypedExpression; +import guru.interlis.model.UniquenessConstraint; +import guru.interlis.model.UnitSymbol; +import guru.interlis.model.ViewSymbol; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +/** Checks and completes translation correspondences on the bound graph. */ +final class TranslationBindings { + private final IndexedDeclarations index; + private final SemanticGraphRead state; + private final SemanticWork work; + private final SemanticDiagnostics diagnostics; + private final Function effectiveDescriptor; + private final BiFunction> findMember; + private final ViewBuilder views; + private final Consumer replaceModel; + + TranslationBindings(IndexedDeclarations index, SemanticGraphRead state, SemanticWork work, + SemanticDiagnostics diagnostics, + Function effectiveDescriptor, + BiFunction> findMember, + ViewBuilder views, Consumer replaceModel) { + this.index = index; + this.state = state; + this.work = work; + this.diagnostics = diagnostics; + this.effectiveDescriptor = effectiveDescriptor; + this.findMember = findMember; + this.views = views; + this.replaceModel = replaceModel; + } + + void complete() { + List models = state.symbolsInBuildOrder().stream() + .filter(ModelSymbol.class::isInstance).map(ModelSymbol.class::cast).toList(); + Map originals = new LinkedHashMap<>(); + for (var model : models) model.translation().ifPresent(translation -> { + var candidates = models.stream().filter(candidate -> candidate.name().value().equals(translation.sourceModel())) + .filter(candidate -> translation.sourceVersion().isEmpty() || translation.sourceVersion().orElseThrow().equals(candidate.modelVersion())).toList(); + if(translation.sourceTarget().isPresent()) originals.put(model.id(),translation.sourceTarget().orElseThrow()); + else if (candidates.size() == 1) originals.put(model.id(), candidates.getFirst().id()); + }); + for (ModelSymbol model : models) { + work.checkpoint(); + if (model.translation().isEmpty()) { + continue; + } + guru.interlis.model.ModelTranslation translation = model.translation().orElseThrow(); + ModelSymbol source = (ModelSymbol) state.symbol(originals.get(model.id())); + DeclInfo decl = index.byId(model.id()).orElse(null); + if (decl != null && translationRoot(model.id(), originals) == null) { + diagnostics.emitRule(decl, "translation dependency has no original model", "TRANSLATION_CYCLE"); + continue; + } + if (source == null) { + if (decl != null) { + diagnostics.emitRule(decl, "translation source model not found", "TRANSLATION_SOURCE"); + } + continue; + } + if (translation.sourceVersion().isPresent() + && !translation.sourceVersion().orElseThrow().equals(source.modelVersion())) { + if (decl != null) { + diagnostics.emitRule(decl, "translation source version mismatch", "TRANSLATION_VERSION"); + } + } + if (decl != null) { + Map correspondence = new LinkedHashMap<>(); + if (!correspondenceWithImports(source, model, originals, correspondence, new HashSet<>())) correspondence.clear(); + if (correspondence.isEmpty() + || !sameModelProfile(source, model)) { + diagnostics.emitRule(decl, "translation syntax does not match source model", + "TRANSLATION_STRUCTURE"); + } + if (!correspondence.isEmpty()) { + var checked = new LinkedHashSet(); + while (true) { + work.checkpoint(); + var pending = correspondence.entrySet().stream().filter(e -> !checked.contains(e.getKey())) + .map(e -> new guru.interlis.model.TranslationLink(e.getKey(), e.getValue())).toList(); + if (pending.isEmpty()) break; + for (var pair : pending) { + work.checkpoint(); + checked.add(pair.original()); + var left=state.symbol(pair.original());var right=state.symbol(pair.translated()); + if(left!=null && right!=null)compareSymbolReferences(left,right,correspondence,decl); + } + } + } + // Expression/formation checks may add verified contextualized declarations. + // Publish the final immutable links only after all newly added pairs were checked. + var bound = new guru.interlis.model.ModelTranslation(translation.sourceModel(), translation.sourceVersion(), translation.language(), + Optional.of(source.id()), correspondence.entrySet().stream().map(e -> new guru.interlis.model.TranslationLink(e.getKey(), e.getValue())).toList(),translation.origin()); + replaceModel.accept(new ModelSymbol(model.header(), model.modelKind(), model.contracted(), model.modelVersion(), + model.issuer(), model.imports(), model.members(), Optional.of(bound), model.options(), model.transferNamespace())); + } + } + } + + private boolean sameModelProfile(ModelSymbol source, ModelSymbol target) { + if (source.modelKind() != target.modelKind() + || source.header().languageVersion() == LanguageVersion.ILI23 && source.contracted() != target.contracted() + || source.options().incrementalTransferAllowed() != target.options().incrementalTransferAllowed() + || !source.options().characterSet().map(v -> CharacterSets.canonical(v.value()).orElse(v.value())) + .equals(target.options().characterSet().map(v -> CharacterSets.canonical(v.value()).orElse(v.value()))) + || source.imports().size() != target.imports().size()) { + return false; + } + return true; + } + + private boolean correspondenceWithImports(ModelSymbol source, ModelSymbol target, + Map originals, Map map, + Set> visited) { + work.checkpoint(); + if (!visited.add(List.of(source.id(), target.id()))) return true; + if (!correspondenceInto(source, target, map) || source.imports().size() != target.imports().size()) return false; + Set used = new HashSet<>(); + for (var imported : source.imports()) { + work.checkpoint(); + List exact = new ArrayList<>(), translated = new ArrayList<>(); + for (int i = 0; i < target.imports().size(); i++) { + if (used.contains(i)) continue; + var candidate = target.imports().get(i); + if (candidate.unqualified() != imported.unqualified()) continue; + if (imported.targetModel().isEmpty() || candidate.targetModel().isEmpty()) { + if (imported.importedModel().equals("INTERLIS") && candidate.importedModel().equals("INTERLIS")) exact.add(i); + } else if (imported.targetModel().equals(candidate.targetModel())) exact.add(i); + else { + SymbolId root = translationRoot(imported.targetModel().orElseThrow(), originals); + if (root != null && root.equals(translationRoot(candidate.targetModel().orElseThrow(), originals))) translated.add(i); + } + } + var candidates = exact.isEmpty() ? translated : exact; + if (candidates.size() != 1) return false; + int selected = candidates.getFirst(); used.add(selected); + if (imported.targetModel().isPresent()) { + var a = state.symbol(imported.targetModel().orElseThrow()); + var b = state.symbol(target.imports().get(selected).targetModel().orElseThrow()); + if (!(a instanceof ModelSymbol x) || !(b instanceof ModelSymbol y) + || !correspondenceWithImports(x, y, originals, map, visited)) return false; + } + } + return true; + } + + private SymbolId translationRoot(SymbolId model, Map originals) { + var visited = new LinkedHashSet(); + var current = model; + while (originals.containsKey(current)) { + work.checkpoint(); + if (!visited.add(current)) return null; + current = originals.get(current); + } + return current; + } + + private boolean correspondenceInto(Symbol source, Symbol target, + Map map) { + if (source.kind() != target.kind()) { + return false; + } + SymbolId previous = map.putIfAbsent(source.id(), target.id()); + if (previous != null && !previous.equals(target.id())) return false; + if (source instanceof DomainSymbol || source instanceof AttributeSymbol) { + var a = index.children(source.id()).stream().filter(d -> d.kind() == SymbolKind.ENUM_ELEMENT).map(DeclInfo::id).toList(); + var b = index.children(target.id()).stream().filter(d -> d.kind() == SymbolKind.ENUM_ELEMENT).map(DeclInfo::id).toList(); + if (!childCorrespondence(a, b, map)) return false; + } + var sourceConstraints=index.children(source.id()).stream().filter(d->d.kind()==SymbolKind.CONSTRAINT).map(DeclInfo::id).toList(); + var targetConstraints=index.children(target.id()).stream().filter(d->d.kind()==SymbolKind.CONSTRAINT).map(DeclInfo::id).toList(); + if(!constraintCorrespondence(sourceConstraints,targetConstraints,map))return false; + if (source instanceof ModelSymbol sourceModel && target instanceof ModelSymbol targetModel) { + return childCorrespondence(sourceModel.members(), targetModel.members(), map); + } + if (source instanceof guru.interlis.model.GraphicSymbol a && target instanceof guru.interlis.model.GraphicSymbol b) + return childCorrespondence(a.rules(), b.rules(), map); + if (source instanceof guru.interlis.model.MetadataBasketSymbol a && target instanceof guru.interlis.model.MetadataBasketSymbol b) + return childCorrespondence(a.objects(), b.objects(), map); + if (source instanceof TopicSymbol sourceTopic && target instanceof TopicSymbol targetTopic) { + return childCorrespondence(sourceTopic.members(), targetTopic.members(), map); + } + if (source instanceof ClassSymbol sourceClass && target instanceof ClassSymbol targetClass) { + if (!childCorrespondence(sourceClass.members(), targetClass.members(), map)) { + return false; + } + return constraintCorrespondence(sourceClass.constraints(), targetClass.constraints(), map); + } + if (source instanceof StructureSymbol sourceStructure + && target instanceof StructureSymbol targetStructure) { + if (!childCorrespondence(sourceStructure.members(), targetStructure.members(), map)) { + return false; + } + return constraintCorrespondence(sourceStructure.constraints(), + targetStructure.constraints(), map); + } + if (source instanceof ViewSymbol a && target instanceof ViewSymbol b) { + if (a.formation().orElse(null) instanceof ViewSymbol.Aggregation + && b.formation().orElse(null) instanceof ViewSymbol.Aggregation) { + var left = findMember.apply(a.id(), "AGGREGATES"); + var right = findMember.apply(b.id(), "AGGREGATES"); + if (left.isEmpty() || right.isEmpty()) return false; + var existingAggregate = map.putIfAbsent(left.orElseThrow(), right.orElseThrow()); + if (existingAggregate != null && !existingAggregate.equals(right.orElseThrow())) return false; + } + return childCorrespondence(a.bases(), b.bases(), map) + && childCorrespondence(a.members(), b.members(), map) + && constraintCorrespondence(a.constraints(), b.constraints(), map); + } + if (source instanceof AssociationSymbol sourceAssociation + && target instanceof AssociationSymbol targetAssociation) { + if (sourceAssociation.derivedFrom().isPresent() != targetAssociation.derivedFrom().isPresent()) return false; + if (sourceAssociation.derivedFrom().isPresent()) { + var existingAssociation = map.putIfAbsent(sourceAssociation.derivedFrom().orElseThrow(), targetAssociation.derivedFrom().orElseThrow()); + if (existingAssociation != null && !existingAssociation.equals(targetAssociation.derivedFrom().orElseThrow())) return false; + } + if (!childCorrespondence(sourceAssociation.roles(), targetAssociation.roles(), map) + || !childCorrespondence(sourceAssociation.attributes(), + targetAssociation.attributes(), map)) { + return false; + } + return constraintCorrespondence(sourceAssociation.constraints(), + targetAssociation.constraints(), map); + } + return true; + } + + private boolean childCorrespondence(List sourceIds, List targetIds, + Map map) { + if (sourceIds.size() != targetIds.size()) { + return false; + } + for (int i = 0; i < sourceIds.size(); i++) { + work.checkpoint(); + Symbol source = state.symbol(sourceIds.get(i)); + Symbol target = state.symbol(targetIds.get(i)); + if (source == null || target == null || !correspondenceInto(source, target, map)) { + return false; + } + } + return true; + } + + private boolean constraintCorrespondence(List sourceIds, List targetIds, + Map map) { + if (sourceIds.size() != targetIds.size()) { + return false; + } + for (int i = 0; i < sourceIds.size(); i++) { + work.checkpoint(); + var previous = map.putIfAbsent(sourceIds.get(i), targetIds.get(i)); + if (previous != null && !previous.equals(targetIds.get(i))) return false; + } + return true; + } + + private void compareSymbolReferences(Symbol source, Symbol target, + Map correspondence, + DeclInfo translationDecl) { + if(!source.header().modifiers().equals(target.header().modifiers())) + diagnostics.emitRule(translationDecl,"translation declaration modifiers differ","TRANSLATION_STRUCTURE"); + if(source instanceof AttributeSymbol a && target instanceof AttributeSymbol b + && (a.mandatory()!=b.mandatory() || a.transientAttribute()!=b.transientAttribute() + || a.continuous()!=b.continuous() || a.subdivision()!=b.subdivision())) + diagnostics.emitRule(translationDecl,"translation attribute flags differ","TRANSLATION_REFERENCE"); + if(source instanceof RoleSymbol a && target instanceof RoleSymbol b + && (!a.cardinality().equals(b.cardinality()) || a.aggregation()!=b.aggregation() + || a.ordered()!=b.ordered() || a.external()!=b.external() || a.hiding()!=b.hiding())) + diagnostics.emitRule(translationDecl,"translation role properties differ","TRANSLATION_REFERENCE"); + if(source instanceof AssociationSymbol a && target instanceof AssociationSymbol b + && (!a.cardinality().equals(b.cardinality()) || !sameMapped(a.derivedFrom(),b.derivedFrom(),correspondence))) + diagnostics.emitRule(translationDecl,"translation association properties differ","TRANSLATION_REFERENCE"); + if(source instanceof DomainSymbol a && target instanceof DomainSymbol b && a.generic()!=b.generic()) + diagnostics.emitRule(translationDecl,"translation domain generic flag differs","TRANSLATION_REFERENCE"); + if(source instanceof TopicSymbol a && target instanceof TopicSymbol b + && !new HashSet<>(a.dependencies().stream().map(id->correspondence.getOrDefault(id,id)).toList()).equals(new HashSet<>(b.dependencies()))) + diagnostics.emitRule(translationDecl,"translation topic dependencies differ","TRANSLATION_REFERENCE"); + if(source instanceof ConstraintSymbol a && target instanceof ConstraintSymbol b + && !constraintHasErrors(a.definition()) && !constraintHasErrors(b.definition())) { + boolean same = sameMapped(a.target(),b.target(),correspondence) + && sameConstraint(a.definition(),b.definition(),correspondence); + var targets=constraintReferenceTargets(a.definition()).stream().map(id->correspondence.getOrDefault(id,id)).toList(); + if(!sameMapped(a.target(),b.target(),correspondence) || !targets.equals(constraintReferenceTargets(b.definition()))) + diagnostics.emitRule(translationDecl,"translation constraint targets differ","TRANSLATION_REFERENCE"); + else if(!same) + diagnostics.emitRule(translationDecl,"translation constraint structure differs","TRANSLATION_CONSTRAINT"); + } + var sourceCrs=Optional.ofNullable(source.header().metaAttributes().values().get("CRS")); + var targetCrs=Optional.ofNullable(target.header().metaAttributes().values().get("CRS")); + if(effectiveDescriptor.apply(state.declaredType(source.id())) instanceof CoordinateType + && !sourceCrs.map(MetaAttributeSet.Value::text).equals(targetCrs.map(MetaAttributeSet.Value::text))) { + var location=targetCrs.map(MetaAttributeSet.Value::origin).filter(Origin.DeclaredOrigin.class::isInstance).map(Origin.DeclaredOrigin.class::cast) + .map(Origin.DeclaredOrigin::range).orElseGet(()->target.origin() instanceof Origin.DeclaredOrigin declared?declared.range():translationDecl.syntax().range()); + diagnostics.emitRule(translationDecl,"translation coordinate reference systems differ","TRANSLATION_COORDINATE",location); + } + if (source instanceof ViewSymbol a && target instanceof ViewSymbol b) { + if (!sameMapped(a.baseView(), b.baseView(), correspondence) + || !viewFormationShape(a).equals(viewFormationShape(b)) + || !sameExpressionLists(a.selections(), b.selections(), correspondence) + || !sameExpressionLists(formationExpressions(a), formationExpressions(b), correspondence) + || !a.baseExtensions().stream().map(e -> List.of(correspondence.getOrDefault(e.base(), e.base()), + e.extensions().stream().map(id -> correspondence.getOrDefault(id, id)).toList())).toList() + .equals(b.baseExtensions().stream().map(e -> List.of(e.base(), e.extensions())).toList())) + diagnostics.emitRule(translationDecl, "translation view formation or selection differs", "TRANSLATION_REFERENCE"); + } + if (source instanceof ViewSymbol a && target instanceof ViewSymbol b + && a.formation().orElse(null) instanceof ViewSymbol.Inspection x + && b.formation().orElse(null) instanceof ViewSymbol.Inspection y + && (!sameExpressionLists(List.of(x.path()),List.of(y.path()),correspondence) + || !sameInspectionTarget(x.element(),y.element(),correspondence) + || !sameInspectionTarget(x.parent(),y.parent(),correspondence))) + diagnostics.emitRule(translationDecl,"translation inspection context differs","TRANSLATION_REFERENCE"); + if (source instanceof ViewSymbol.Base a && target instanceof ViewSymbol.Base b + && (a.orNull() != b.orNull() || !correspondence.getOrDefault(a.target(), a.target()).equals(b.target()))) + diagnostics.emitRule(translationDecl, "translation view basis differs", "TRANSLATION_REFERENCE"); + if (source instanceof guru.interlis.model.GraphicSymbol a && target instanceof guru.interlis.model.GraphicSymbol b + && (!sameMapped(a.baseGraphic(),b.baseGraphic(),correspondence) || !sameMapped(a.basis(),b.basis(),correspondence) + || !sameExpressionLists(a.selections(),b.selections(),correspondence))) + diagnostics.emitRule(translationDecl,"translated graphic basis or selections differ","TRANSLATION_GRAPHIC"); + if (source instanceof guru.interlis.model.DrawingRuleSymbol a && target instanceof guru.interlis.model.DrawingRuleSymbol b) { + boolean same=sameMapped(a.signatureClass(),b.signatureClass(),correspondence) && sameMapped(a.overrides(),b.overrides(),correspondence) + && a.clauses().size()==b.clauses().size(); + for(int i=0;same && i correspondence.getOrDefault(id, id)).toList() + .equals(right.restrictions()); + } + if (!same) { + diagnostics.emitRule(translationDecl, "translation role target differs", + "TRANSLATION_REFERENCE"); + } + } + if (source instanceof guru.interlis.model.MetadataBasketSymbol a && target instanceof guru.interlis.model.MetadataBasketSymbol b + && (a.basketKind() != b.basketKind() || !sameMapped(a.topic(), b.topic(), correspondence) || !sameMapped(a.baseBasket(), b.baseBasket(), correspondence))) + diagnostics.emitRule(translationDecl, "translated metadata basket has different bindings", "TRANSLATION_METADATA_BASKET"); + if (source instanceof guru.interlis.model.MetaObjectSymbol a && target instanceof guru.interlis.model.MetaObjectSymbol b + && (!a.name().equals(b.name()) || !sameMapped(a.objectClass(), b.objectClass(), correspondence))) + diagnostics.emitRule(translationDecl, "translated metadata object has a different name or class", "TRANSLATION_METAOBJECT"); + if (source instanceof guru.interlis.model.LineFormSymbol a && target instanceof guru.interlis.model.LineFormSymbol b + && !sameMapped(a.segmentStructure(), b.segmentStructure(), correspondence)) + diagnostics.emitRule(translationDecl, "translated line form targets a different segment structure", "TRANSLATION_LINE_FORM"); + if (source instanceof UnitSymbol sourceUnit && target instanceof UnitSymbol targetUnit) { + boolean same = sourceUnit.abstractUnit() == targetUnit.abstractUnit() + && sameMapped(sourceUnit.base(), targetUnit.base(), correspondence) + && sameMapped(sourceUnit.derivedFrom(), targetUnit.derivedFrom(), correspondence) + && sourceUnit.components().size() == targetUnit.components().size() + && sameUnitConversion(sourceUnit, targetUnit); + if (same) for (int i = 0; i < sourceUnit.components().size(); i++) { + var a = sourceUnit.components().get(i); var b = targetUnit.components().get(i); + if (a.operator() != b.operator() || !sameMapped(Optional.of(a.unit()), Optional.of(b.unit()), correspondence)) same = false; + } + if (!same) { + diagnostics.emitRule(translationDecl, "translation unit base differs", + "TRANSLATION_REFERENCE"); + } + } + if (source instanceof guru.interlis.model.ContextSymbol original && target instanceof guru.interlis.model.ContextSymbol translated) { + boolean same = original.mappings().size() == translated.mappings().size(); + for (int i = 0; same && i < original.mappings().size(); i++) { + var left = original.mappings().get(i); var right = translated.mappings().get(i); + same = correspondence.getOrDefault(left.genericDomain(), left.genericDomain()).equals(right.genericDomain()) + && left.alternatives().stream().map(id -> correspondence.getOrDefault(id, id)).toList().equals(right.alternatives()); + } + if (!same) diagnostics.emitRule(translationDecl, "translation context targets differ", "TRANSLATION_REFERENCE"); + } + if (source instanceof TopicSymbol original && target instanceof TopicSymbol translated + && !original.deferredGenerics().stream().map(id -> correspondence.getOrDefault(id, id)).toList().equals(translated.deferredGenerics())) + diagnostics.emitRule(translationDecl, "translation deferred domains differ", "TRANSLATION_REFERENCE"); + if (source instanceof DomainSymbol sourceDomain + && target instanceof DomainSymbol targetDomain) { + if (!sameMapped(sourceDomain.baseDomain(), targetDomain.baseDomain(), + correspondence)) { + diagnostics.emitRule(translationDecl, "translation domain base differs", + "TRANSLATION_REFERENCE"); + } + if (!sameMappedType(sourceDomain.typeId(), targetDomain.typeId(), + correspondence)) { + diagnostics.emitRule(translationDecl, "translation domain type differs", + "TRANSLATION_REFERENCE"); + } + } + } + + private boolean sameExpressionLists(List source, List target, + Map correspondence) { + if (source.size() != target.size()) return false; + for (int i = 0; i < source.size(); i++) { + work.checkpoint(); + if (!sameExpression(source.get(i),target.get(i),correspondence)) return false; + var expected = expressionReferenceTargets(source.get(i)).stream() + .map(id -> correspondence.getOrDefault(id, id)).toList(); + if (!expected.equals(expressionReferenceTargets(target.get(i)))) return false; + } + return true; + } + + private List formationExpressions(ViewSymbol view) { + return view.formation().map(f -> switch (f) { + case ViewSymbol.Aggregation a -> a.equal(); + case ViewSymbol.Inspection i -> List.of(i.path()); + default -> List.of(); + }).orElse(List.of()); + } + + private String viewFormationShape(ViewSymbol view) { + return view.formation().map(f -> switch (f) { + case ViewSymbol.Projection ignored -> "PROJECTION"; + case ViewSymbol.Join j -> "JOIN:" + j.bases().size(); + case ViewSymbol.Union u -> "UNION:" + u.bases().size(); + case ViewSymbol.Aggregation a -> "AGGREGATION:" + a.equal().size(); + case ViewSymbol.Inspection i -> "INSPECTION:" + i.area(); + }).orElse("NONE"); + } + + private boolean sameMapped(Optional source, Optional target, + Map correspondence) { + if (source.isEmpty() || target.isEmpty()) { + return source.isEmpty() && target.isEmpty(); + } + SymbolId mapped = correspondence.getOrDefault(source.orElseThrow(), source.orElseThrow()); + return mapped.equals(target.orElseThrow()); + } + + private boolean sameAttributePathOwner(Optional a, + Optional b, Map correspondence) { + if (a.isEmpty() || b.isEmpty()) return a.isEmpty() && b.isEmpty(); + if (a.orElseThrow() instanceof guru.interlis.model.AttributePathType.ClassAttribute x + && b.orElseThrow() instanceof guru.interlis.model.AttributePathType.ClassAttribute y) + return sameExpressionLists(List.of(x.path()), List.of(y.path()), correspondence); + if (a.orElseThrow() instanceof guru.interlis.model.AttributePathType.Argument x + && b.orElseThrow() instanceof guru.interlis.model.AttributePathType.Argument y) + return x.index() == y.index() && sameMapped(Optional.of(x.function()), Optional.of(y.function()), correspondence); + return a.orElseThrow() instanceof guru.interlis.model.AttributePathType.Viewable x + && b.orElseThrow() instanceof guru.interlis.model.AttributePathType.Viewable y + && sameMapped(Optional.of(x.target()), Optional.of(y.target()), correspondence); + } + + private boolean sameMappedType(TypeId source, TypeId target, + Map correspondence) { + work.checkpoint(); + TypeDescriptor sourceType = state.type(source); + TypeDescriptor targetType = state.type(target); + if(sourceType==null || targetType==null || sourceType.getClass()!=targetType.getClass())return false; + if(sourceType instanceof TextType a && targetType instanceof TextType b) + return a.textKind()==b.textKind() && a.maxLength().equals(b.maxLength()); + if(sourceType instanceof AlignmentType a && targetType instanceof AlignmentType b)return a.alignment()==b.alignment(); + if(sourceType instanceof BlackboxType a && targetType instanceof BlackboxType b)return a.blackboxKind()==b.blackboxKind(); + if(sourceType instanceof guru.interlis.model.EnumArgumentType a && targetType instanceof guru.interlis.model.EnumArgumentType b)return a.treeValues()==b.treeValues(); + if(sourceType instanceof guru.interlis.model.GenericType a && targetType instanceof guru.interlis.model.GenericType b)return a.name().equals(b.name()); + if(sourceType instanceof EnumerationType a && targetType instanceof EnumerationType b) + return a.ordered()==b.ordered() && a.circular()==b.circular() && a.treeValues()==b.treeValues() + && sameEnumBase(a.baseType(),b.baseType(),correspondence) && enumParents(a).equals(enumParents(b)) && enumFinals(a).equals(enumFinals(b)); + if(sourceType instanceof FormattedType a && targetType instanceof FormattedType b) { + if(!a.predefined().equals(b.predefined()) || !sameMapped(a.baseDomain(),b.baseDomain(),correspondence) + || !sameMapped(a.structure(),b.structure(),correspondence) || !a.minimum().equals(b.minimum()) + || !a.maximum().equals(b.maximum()) || a.parts().size()!=b.parts().size())return false; + for(int i=0;i correspondence.getOrDefault(id, id)).toList().equals(b.restrictions()); + if (sourceType instanceof ClassValueType a && targetType instanceof ClassValueType b) + return a.structureValue() == b.structureValue() && a.restrictions().stream() + .map(id -> correspondence.getOrDefault(id, id)).toList().equals(b.restrictions()); + if (sourceType instanceof ReferenceType sourceReference + && targetType instanceof ReferenceType targetReference) { + return sourceReference.external() == targetReference.external() + && sameMapped(sourceReference.target(), targetReference.target(), correspondence) + && sourceReference.restrictions().size() == targetReference.restrictions().size() + && java.util.stream.IntStream.range(0, sourceReference.restrictions().size()).allMatch(i -> + sameMapped(Optional.of(sourceReference.restrictions().get(i)), + Optional.of(targetReference.restrictions().get(i)), correspondence)); + } + if (sourceType instanceof StructureValueType sourceStructure + && targetType instanceof StructureValueType targetStructure) { + return sameMapped(sourceStructure.target(), targetStructure.target(), correspondence) + && sourceStructure.restrictions().stream().map(t -> correspondence.getOrDefault(t,t)).toList().equals(targetStructure.restrictions()); + } + if (sourceType instanceof DomainReferenceType sourceDomain + && targetType instanceof DomainReferenceType targetDomain) { + return sameMapped(Optional.of(sourceDomain.targetDomain()), + Optional.of(targetDomain.targetDomain()), correspondence); + } + if (sourceType instanceof CollectionType sourceCollection + && targetType instanceof CollectionType targetCollection) { + return sourceCollection.collectionKind()==targetCollection.collectionKind() + && sourceCollection.cardinality().equals(targetCollection.cardinality()) + && sameMappedType(sourceCollection.elementType(), targetCollection.elementType(), + correspondence); + } + if (sourceType instanceof GeometryType sourceGeometry + && targetType instanceof GeometryType targetGeometry) { + return sourceGeometry.geometryKind()==targetGeometry.geometryKind() + && sameMapped(sourceGeometry.vertexDomain(), targetGeometry.vertexDomain(), correspondence) + && sameMapped(sourceGeometry.lineAttributes(), targetGeometry.lineAttributes(), correspondence) + && sourceGeometry.directed() == targetGeometry.directed() + && sourceGeometry.withoutOverlaps() == targetGeometry.withoutOverlaps() + && sameGeometryTolerance(sourceGeometry.overlap(), targetGeometry.overlap(), correspondence) + && sourceGeometry.lineForms().stream().map(x -> correspondence.getOrDefault(x, x)) + .collect(java.util.stream.Collectors.toSet()).equals(new HashSet<>(targetGeometry.lineForms())); + } + if (sourceType instanceof NumericType a && targetType instanceof NumericType b) + return sameNumeric(a.range(), b.range(), correspondence); + if (sourceType instanceof CoordinateType sourceCoordinate + && targetType instanceof CoordinateType targetCoordinate) { + if (sourceCoordinate.multiCoordinate()!=targetCoordinate.multiCoordinate() || !sourceCoordinate.rotation().equals(targetCoordinate.rotation()) || sourceCoordinate.dimensions() != targetCoordinate.dimensions() + || !sourceCoordinate.referenceSystem().equals( + targetCoordinate.referenceSystem())) { + return false; + } + for (int i = 0; i < sourceCoordinate.axes().size(); i++) { + work.checkpoint(); + NumericRange sourceAxis = sourceCoordinate.axes().get(i); + NumericRange targetAxis = targetCoordinate.axes().get(i); + if (!sameNumeric(sourceAxis, targetAxis, correspondence)) { + return false; + } + } + return true; + } + return sourceType instanceof BooleanType; + } + + private boolean sameEnumBase(Optional source,Optional target,Map map) { + if(source.isEmpty() || target.isEmpty())return source.isEmpty() && target.isEmpty(); + var a=state.declaredTypesInBuildOrder().stream().filter(e->e.getValue().equals(source.orElseThrow())).map(Map.Entry::getKey).findFirst(); + var b=state.declaredTypesInBuildOrder().stream().filter(e->e.getValue().equals(target.orElseThrow())).map(Map.Entry::getKey).findFirst(); + return a.isPresent() && b.isPresent() ? sameMapped(a,b,map) : sameMappedType(source.orElseThrow(),target.orElseThrow(),map); + } + + /** Enum shape uses parent ordinals, never translated names or an arbitrary nesting limit. */ + private List enumParents(EnumerationType enumeration) { + var paths=enumeration.values().stream().map(EnumerationValue::path).toList();var result=new ArrayList(); + for(var path:paths){work.checkpoint();int dot=path.lastIndexOf('.');result.add(dot<0?-1:paths.indexOf(path.substring(0,dot)));} + return result; + } + private Set enumFinals(EnumerationType enumeration) { + var paths=enumeration.values().stream().map(EnumerationValue::path).toList();var result=new HashSet(); + for(var path:enumeration.finalSubtrees()){work.checkpoint();result.add(path.isEmpty()?-1:paths.indexOf(path));} + return result; + } + + private boolean sameDecimal(Optional a,Optional b) { + if(a.isEmpty() || b.isEmpty())return a.isEmpty() && b.isEmpty(); + var x=a.orElseThrow();var y=b.orElseThrow(); + return x.exactValue().compareTo(y.exactValue())==0 + && (long)x.precision().exponent()-x.precision().fractionalDigits()==(long)y.precision().exponent()-y.precision().fractionalDigits(); + } + + private boolean sameGeometryTolerance(Optional source, Optional target, + Map correspondence) { + if (source.orElse(null) instanceof GeometryType.TrackWidth a && target.orElse(null) instanceof GeometryType.TrackWidth b) + return correspondence.getOrDefault(a.coordinateDomain(), a.coordinateDomain()).equals(b.coordinateDomain()); + if (source.orElse(null) instanceof GeometryType.Explicit a && target.orElse(null) instanceof GeometryType.Explicit b) + return a.value().exactValue().compareTo(b.value().exactValue()) == 0; + return source.isEmpty() && target.isEmpty(); + } + + private boolean sameNumeric(NumericRange a, NumericRange b, Map map) { + if (!sameDecimal(a.minimum(),b.minimum()) || !sameDecimal(a.maximum(),b.maximum()) || a.circular() != b.circular() + || a.direction() != b.direction() || !sameMapped(a.unit(), b.unit(), map) || a.referenceSystem().isPresent() != b.referenceSystem().isPresent()) return false; + return sameReferenceSystem(a.referenceSystem(),b.referenceSystem(),map); + } + private boolean sameReferenceSystem(Optional a,Optional b,Map map) { + if(a.isEmpty() || b.isEmpty())return a.isEmpty() && b.isEmpty(); + var x=a.orElseThrow();var y=b.orElseThrow(); + return x.domain() == y.domain() && x.axis().equals(y.axis()) && map.getOrDefault(x.target(), x.target()).equals(y.target()); + } + + private List constraintReferenceTargets(ConstraintDefinition definition) { + List ids = new ArrayList<>(); + List targets = new ArrayList<>(); + switch (definition) { + case MandatoryConstraint c -> ids.add(c.condition()); + case DomainConstraint c -> ids.add(c.condition()); + case PlausibilityConstraint c -> ids.add(c.condition()); + case guru.interlis.model.SetConstraint c -> { c.where().ifPresent(ids::add); ids.add(c.condition()); } + case UniquenessConstraint c -> { + c.where().ifPresent(ids::add); ids.addAll(c.keys()); + for (var local : c.localKeys()) { ids.add(local.prefix()); ids.addAll(local.fields()); } + } + case guru.interlis.model.ExistenceConstraint c -> { + ids.add(c.source()); + for (var target : c.targets()) { targets.add(target.viewable()); ids.add(target.path()); } + } + } + for (var id : ids) targets.addAll(expressionReferenceTargets(id)); + return targets; + } + + private List expressionReferenceTargets(ExpressionId id) { + if (id == null) { + return List.of(); + } + TypedExpression expression = state.expression(id); + if (expression == null) { + return List.of(); + } + List targets = new ArrayList<>(); + if (expression instanceof guru.interlis.model.PathExpression path) { + for (guru.interlis.model.PathStep step : path.path().steps()) { + work.checkpoint(); + if (step instanceof guru.interlis.model.PathStep.ViewBaseStep base) { + targets.add(base.base()); targets.add(base.target()); + } else if (step instanceof guru.interlis.model.PathStep.InspectionStep inspected) { + targets.add(inspected.view()); targets.add(inspected.target()); + } else if (step instanceof guru.interlis.model.PathStep.AttributeStep attribute) { + targets.add(attribute.target()); + } else if (step instanceof guru.interlis.model.PathStep.RoleStep role) { + targets.add(role.target()); + } else if (step instanceof guru.interlis.model.PathStep.AssociationAccessStep access) { + targets.add(access.association()); targets.add(access.sourceRole()); targets.add(access.targetRole()); + } else if (step instanceof guru.interlis.model.PathStep.AssociationSourceStep access) { + targets.add(access.association()); targets.add(access.sourceRole()); + } + } + } + if(expression instanceof guru.interlis.model.FunctionCallExpression call) { + call.targetFunction().ifPresent(targets::add); + for(var path:call.argumentPaths().values())path.binding().ifPresent(id2->targets.addAll(expressionReferenceTargets(id2))); + } + if (expression instanceof guru.interlis.model.SymbolValueExpression value) targets.add(value.target()); + if (expression instanceof guru.interlis.model.InspectionExpression inspected) { + inspected.view().ifPresent(targets::add); targets.add(inspected.source()); targets.add(inspected.element()); + } + if (expression instanceof guru.interlis.model.ObjectSetExpression + && state.type(expression.typeId()) instanceof guru.interlis.model.ObjectArgumentType set) { + set.base().ifPresent(targets::add); targets.addAll(set.restrictions()); + } + for (ExpressionId child : expression.children()) { + work.checkpoint(); + targets.addAll(expressionReferenceTargets(child)); + } + return targets; + } + + private boolean sameConstraint(ConstraintDefinition source,ConstraintDefinition target,Map map) { + if(source.getClass()!=target.getClass())return false; + if(!sameExpressionLists(constraintExpressions(source),constraintExpressions(target),map))return false; + if(source instanceof PlausibilityConstraint a && target instanceof PlausibilityConstraint b + && (a.direction()!=b.direction() || a.percentage().compareTo(b.percentage())!=0))return false; + if(source instanceof guru.interlis.model.SetConstraint a && target instanceof guru.interlis.model.SetConstraint b + && (a.scope()!=b.scope() || a.where().isPresent()!=b.where().isPresent()))return false; + if(source instanceof UniquenessConstraint a && target instanceof UniquenessConstraint b + && (a.basket()!=b.basket() || a.where().isPresent()!=b.where().isPresent() + || a.keys().size()!=b.keys().size() || a.localKeys().size()!=b.localKeys().size() + || !a.localKeys().stream().map(k->k.fields().size()).toList().equals(b.localKeys().stream().map(k->k.fields().size()).toList())))return false; + if(source instanceof guru.interlis.model.ExistenceConstraint a && target instanceof guru.interlis.model.ExistenceConstraint b + && a.targets().size()!=b.targets().size())return false; + var originalTargets=constraintReferenceTargets(source).stream().map(id->map.getOrDefault(id,id)).toList(); + if(!originalTargets.equals(constraintReferenceTargets(target)))return false; + return true; + } + + private boolean constraintHasErrors(ConstraintDefinition definition) { + return constraintExpressions(definition).stream().map(state::expression) + .anyMatch(e->e!=null&&e.typeState()==guru.interlis.model.ExpressionTypeState.ERROR); + } + + private List constraintExpressions(ConstraintDefinition definition) { + var ids=new ArrayList(); + switch(definition) { + case MandatoryConstraint c -> ids.add(c.condition()); + case DomainConstraint c -> ids.add(c.condition()); + case PlausibilityConstraint c -> ids.add(c.condition()); + case guru.interlis.model.SetConstraint c -> {c.where().ifPresent(ids::add);ids.add(c.condition());} + case UniquenessConstraint c -> {c.where().ifPresent(ids::add);ids.addAll(c.keys());for(var k:c.localKeys()){ids.add(k.prefix());ids.addAll(k.fields());}} + case guru.interlis.model.ExistenceConstraint c -> {ids.add(c.source());for(var t:c.targets())ids.add(t.path());} + } + return ids; + } + + /** Structural expression comparison uses bound identities; documentary spelling is irrelevant. */ + boolean sameBoundExpression(ExpressionId source,ExpressionId target) { + return sameExpression(source,target,Map.of()); + } + + private boolean sameExpression(ExpressionId source,ExpressionId target,Map map) { + work.checkpoint();var a=state.expression(source);var b=state.expression(target); + if(a==null || b==null || a.getClass()!=b.getClass())return false; + if(a instanceof guru.interlis.model.LiteralExpression x && b instanceof guru.interlis.model.LiteralExpression y) { + if(x.literalKind()!=y.literalKind())return false; + return switch(x.literalKind()) { + case NUMBER -> x.decimal().isPresent() && y.decimal().isPresent() + && x.decimal().orElseThrow().exactValue().compareTo(y.decimal().orElseThrow().exactValue())==0 + && sameNumericContext(x.typeId(),y.typeId(),map); + case TEXT -> x.textValue().equals(y.textValue()); + case BOOLEAN -> x.lexeme().equals(y.lexeme()); + }; + } + if(a instanceof guru.interlis.model.BinaryExpression x && b instanceof guru.interlis.model.BinaryExpression y && x.operator()!=y.operator())return false; + if(a instanceof guru.interlis.model.UnaryExpression x && b instanceof guru.interlis.model.UnaryExpression y && x.operator()!=y.operator())return false; + if(a instanceof guru.interlis.model.FunctionCallExpression x && b instanceof guru.interlis.model.FunctionCallExpression y) { + if(!sameMapped(x.targetFunction(),y.targetFunction(),map) + || x.targetFunction().isEmpty() && !x.functionName().equals(y.functionName()) + || !x.argumentPaths().keySet().equals(y.argumentPaths().keySet()) || x.children().size()!=y.children().size())return false; + for(int i=0;iList.of(v.name(),v.associationAccess(),v.associationQualifier(),v.index())).toList() + .equals(q.steps().stream().map(v->List.of(v.name(),v.associationAccess(),v.associationQualifier(),v.index())).toList()))return false; + } + } + return true; + } + if(a instanceof guru.interlis.model.PathExpression x && b instanceof guru.interlis.model.PathExpression y) { + if(x.path().steps().size()!=y.path().steps().size())return false; + for(int i=0;i map) { + var a=effectiveDescriptor.apply(source);var b=effectiveDescriptor.apply(target); + if(a instanceof NumericType x && b instanceof NumericType y) + return sameMapped(x.range().unit(),y.range().unit(),map) + && sameReferenceSystem(x.range().referenceSystem(),y.range().referenceSystem(),map); + return a!=null && b!=null && a.getClass()==b.getClass(); + } + private int enumValueIndex(EnumerationType enumeration,String path) { + return enumeration.values().stream().map(EnumerationValue::path).toList().indexOf(path); + } + private boolean samePathStep(guru.interlis.model.PathStep source,guru.interlis.model.PathStep target,Map map) { + work.checkpoint();if(source.getClass()!=target.getClass())return false; + return switch(source) { + case guru.interlis.model.PathStep.AttributeStep a -> sameInspectionTarget(a.target(),((guru.interlis.model.PathStep.AttributeStep)target).target(),map); + case guru.interlis.model.PathStep.RoleStep a -> sameInspectionTarget(a.target(),((guru.interlis.model.PathStep.RoleStep)target).target(),map); + case guru.interlis.model.PathStep.IndexStep a -> a.index().equals(((guru.interlis.model.PathStep.IndexStep)target).index()); + case guru.interlis.model.PathStep.ViewBaseStep a -> {var b=(guru.interlis.model.PathStep.ViewBaseStep)target;yield sameInspectionTarget(a.base(),b.base(),map) && sameInspectionTarget(a.target(),b.target(),map);} + case guru.interlis.model.PathStep.InspectionStep a -> {var b=(guru.interlis.model.PathStep.InspectionStep)target;yield a.kind()==b.kind() && sameInspectionTarget(a.view(),b.view(),map) && sameInspectionTarget(a.target(),b.target(),map);} + case guru.interlis.model.PathStep.AssociationAccessStep a -> {var b=(guru.interlis.model.PathStep.AssociationAccessStep)target;yield a.associationObject()==b.associationObject() && a.cardinality().equals(b.cardinality()) + && sameInspectionTarget(a.association(),b.association(),map) && sameInspectionTarget(a.sourceRole(),b.sourceRole(),map) && sameInspectionTarget(a.targetRole(),b.targetRole(),map);} + case guru.interlis.model.PathStep.AssociationSourceStep a -> {var b=(guru.interlis.model.PathStep.AssociationSourceStep)target;yield a.cardinality().equals(b.cardinality()) && sameInspectionTarget(a.association(),b.association(),map) && sameInspectionTarget(a.sourceRole(),b.sourceRole(),map);} + case guru.interlis.model.PathStep.ThisStep ignored -> true; + case guru.interlis.model.PathStep.ParentStep ignored -> true; + case guru.interlis.model.PathStep.UnresolvedStep ignored -> false; + }; + } + + /** A registered geometry context may extend correspondence only after its actual + * bound geometry agrees. No generated identifier or display name is interpreted. */ + private boolean sameInspectionTarget(SymbolId source, SymbolId target, Map map) { + if (map.getOrDefault(source,source).equals(target)) return true; + var a=views.geometryContext(source);var b=views.geometryContext(target); + if (a==null || b==null + || !map.getOrDefault(a.segments().contextOwner(),a.segments().contextOwner()).equals(b.segments().contextOwner()) + || !sameMappedType(a.geometry().id(),b.geometry().id(),map)) return false; + var left=a.segments().contextualSymbols();var right=b.segments().contextualSymbols(); + if(left.size()!=right.size())return false; + var candidate=new LinkedHashMap<>(map); + for(var pair:left.entrySet()) { + work.checkpoint(); + var original=map.getOrDefault(pair.getKey(),pair.getKey()); + var translated=right.get(original); + if(translated==null)return false; + var previous=candidate.putIfAbsent(pair.getValue(),translated); + if(previous!=null && !previous.equals(translated))return false; + } + var leftWrappers=views.geometryWrappers(a);var rightWrappers=views.geometryWrappers(b); + if(!leftWrappers.keySet().equals(rightWrappers.keySet()))return false; + for(var role:leftWrappers.keySet()) { + work.checkpoint(); + var original=state.symbol(leftWrappers.get(role));var translated=state.symbol(rightWrappers.get(role)); + if(original==null || translated==null || !correspondenceInto(original,translated,candidate))return false; + } + if(!candidate.getOrDefault(source,source).equals(target))return false; + // New context copies are bijective. Existing ordinary correspondences are + // never replaced, and two different copies cannot collapse onto one target. + var usedTargets = new HashMap(); + map.forEach((original,translated) -> usedTargets.putIfAbsent(translated,original)); + for(var pair:candidate.entrySet()) if(!map.containsKey(pair.getKey())) { + work.checkpoint();var previous=usedTargets.putIfAbsent(pair.getValue(),pair.getKey()); + if(previous!=null && !previous.equals(pair.getKey()))return false; + } + // Validation is transactional: a rejected context cannot poison later comparisons. + candidate.forEach(map::putIfAbsent); + return true; + } + + private boolean sameUnitConversion(UnitSymbol left, UnitSymbol right) { + if (left.conversionFunction().isPresent() != right.conversionFunction().isPresent() + || left.conversionFactors().size() != right.conversionFactors().size()) return false; + for (int i = 0; i < left.conversionFactors().size(); i++) { + work.checkpoint(); + var a = left.conversionFactors().get(i); var b = right.conversionFactors().get(i); + if (a.operator() != b.operator() || a.kind() != b.kind() + || a.decimal().isPresent() != b.decimal().isPresent() + || a.decimal().isPresent() && a.decimal().get().exactValue().compareTo(b.decimal().get().exactValue()) != 0) + return false; + } + return true; + } + + private boolean sameDrawingValue(guru.interlis.model.DrawingRuleSymbol.Value left,guru.interlis.model.DrawingRuleSymbol.Value right,Map map) { + if(left instanceof guru.interlis.model.DrawingRuleSymbol.ExpressionValue a && right instanceof guru.interlis.model.DrawingRuleSymbol.ExpressionValue b) + return sameExpressionLists(List.of(a.expression()),List.of(b.expression()),map); + if(left instanceof guru.interlis.model.DrawingRuleSymbol.MetaObjectValue a && right instanceof guru.interlis.model.DrawingRuleSymbol.MetaObjectValue b) + return sameMapped(a.object(),b.object(),map); + if(left instanceof guru.interlis.model.DrawingRuleSymbol.AccordingValue a && right instanceof guru.interlis.model.DrawingRuleSymbol.AccordingValue b) { + if(!sameExpressionLists(List.of(a.selector()),List.of(b.selector()),map) || a.cases().size()!=b.cases().size()) return false; + var at=effectiveDescriptor.apply(state.expression(a.selector()).typeId()); var bt=effectiveDescriptor.apply(state.expression(b.selector()).typeId()); + if(!(at instanceof EnumerationType ae && bt instanceof EnumerationType be)) return false; + var ap=ae.values().stream().map(guru.interlis.model.EnumerationValue::path).toList(); var bp=be.values().stream().map(guru.interlis.model.EnumerationValue::path).toList(); + for(int i=0;i expressions; + private final PredefinedTemporal temporal; + private final StructureBindings structures; + private final MetadataBindings metadata; + private final ReferenceBindings references; + private final FormattedTypeBindings formats; + private final Supplier declarations; + private final Consumer definePredefinedDomain; + private final Set resolvingDomainTypes = new HashSet<>(); + private final Set resolvingAttributeTypes = new HashSet<>(); + private final Map functionArgumentTypes = new LinkedHashMap<>(); + private final Set resolvingFunctionArguments = new HashSet<>(); + private final Map> inferredDerivations = new LinkedHashMap<>(); + private final Map booleanTypes = new java.util.EnumMap<>(guru.interlis.model.LanguageVersion.class); + + TypeBindings(SemanticGraphRead.Types state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + UnitBindings units, + NameBindings names, + InheritanceBindings inheritance, + LineForms lineForms, + ObjectPathBindings paths, + Supplier expressions, + PredefinedTemporal temporal, + StructureBindings structures, + MetadataBindings metadata, + ReferenceBindings references, + FormattedTypeBindings formats, + Supplier declarations, + Consumer definePredefinedDomain) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.units = units; + this.names = names; + this.inheritance = inheritance; + this.lineForms = lineForms; + this.paths = paths; + this.expressions = expressions; + this.temporal = temporal; + this.structures = structures; + this.metadata = metadata; + this.references = references; + this.formats = formats; + this.declarations = declarations; + this.definePredefinedDomain = definePredefinedDomain; + } + + guru.interlis.model.TypeRelation extension(TypeDescriptor derived, TypeDescriptor base, guru.interlis.model.LanguageVersion language) { + return guru.interlis.model.internal.TypeCompatibility.extend(derived,base,language,this::effectiveDescriptor, + state::baseOf,id->effectiveDescriptor(state.declaredType(id)),units::extensionUnit); + } + + TypeId functionArgumentType(DeclInfo function, int position) { + var key = new guru.interlis.model.AttributePathType.Argument(function.id(), position); + TypeId bound = functionArgumentTypes.get(key); + if (bound != null) return bound; + if (!resolvingFunctionArguments.add(key)) { + TypeId error = nextTypeId(); + state.defineType(new ErrorType(error, function.origin(), List.of(diagnostics.emitRule(function, + "cyclic attribute-path argument dependency", "FUNCTION_ARGUMENT_CYCLE")))); + return error; + } + var syntax = (FunctionDeclarationSyntax) function.syntax(); + TypeId type = buildType(syntax.parameters().get(position).type(), function.id(), function.sourceId()); + functionArgumentTypes.put(key, type); resolvingFunctionArguments.remove(key); + return type; + } + + /** Bind referenced types before expressions without recursively building their constraints. */ + TypeId domainType(DeclInfo decl) { + TypeId existing = state.declaredType(decl.id()); + if (existing != null) return existing; + work.checkpoint(); + if (!resolvingDomainTypes.add(decl.id())) { + TypeId error = nextTypeId(); + state.defineType(new ErrorType(error, decl.origin(), + List.of(diagnostics.emitRule(decl, "cyclic domain type dependency", "DOMAIN_TYPE_CYCLE")))); + state.bindDeclaredType(decl.id(), error); + return error; + } + try { + DomainDeclarationSyntax syntax = (DomainDeclarationSyntax) decl.syntax(); + Optional base=syntax.baseDomain().flatMap(name->names.bindTarget(decl,name,Set.of(SymbolKind.DOMAIN),ReferenceUsage.BASE_TYPE,new ArrayList<>())); + + base.ifPresent(id -> { + inheritance.bindBase(decl.id(), id); + index.byId(id).ifPresent(this::domainType); + if ((state.symbol(id) != null && state.symbol(id).header().has(guru.interlis.model.Modifier.FINAL)) + || index.byId(id).filter(d -> d.modifiers().contains(guru.interlis.model.Modifier.FINAL)).isPresent()) + diagnostics.emitRule(decl, "cannot extend FINAL domain", "DOMAIN_FINAL"); + }); + TypeId type; + if (syntax.type() instanceof GenericTypeSyntax generic && "INHERITED".equals(generic.name()) && base.isPresent()) { + type = nextTypeId(); + state.defineType(new DomainReferenceType(type, new Origin.DeclaredOrigin(syntax.type().range()), base.orElseThrow())); + } else type = buildType(syntax.type(), decl.id(), decl.sourceId()); + TypeDescriptor inherited = base.map(state::declaredType).map(this::effectiveDescriptor).orElse(null); + inheritTypeProperties(type, syntax.type(), inherited,decl); + if (syntax.type() instanceof LineTypeSyntax) lineForms.effective(type, inherited, decl); + else if (inherited instanceof GeometryType && !(effectiveDescriptor(type) instanceof GeometryType)) + diagnostics.emitRule(decl, "geometry base requires a geometric extension", "GEOMETRY_EXTENSION"); + if (effectiveDescriptor(type) instanceof NumericType numeric && inherited instanceof NumericType parent) { + var relation = units.numericExtension(numeric, parent, decl.language()); + if (relation.decision() == guru.interlis.model.RelationDecision.DISALLOWED) + diagnostics.emitRule(decl, "numeric type does not specialize its base: " + relation.reason(), relation.reason().startsWith("numeric-unit") || relation.reason().contains("unit-") ? "NUMERIC_UNIT_EXTENSION" : "NUMERIC_EXTENSION"); + } + // Recursive cycle recovery can assign the declaration an error type while its descriptor is built. + TypeId assigned = state.declaredType(decl.id()); + if (assigned == null) { + state.bindDeclaredType(decl.id(), type); + assigned = type; + } else if (!(state.type(assigned) instanceof ErrorType error && error.origin().equals(decl.origin()))) { + throw new IllegalStateException("Domain type already bound: " + decl.id()); + } + if (syntax.type() instanceof EnumerationTypeSyntax enumeration) { + if (base.isPresent() && inherited == null) diagnostics.emitRule(decl, "enumeration base must already be defined", "ENUM_BASE"); + state.replaceType(effectiveEnumeration(type, enumeration, inherited, decl)); + } + TypeDescriptor own=effectiveDescriptor(type); + if(inherited!=null && !(inherited instanceof NumericType && own instanceof NumericType) + && !(own instanceof GeometryType) && !(inherited instanceof guru.interlis.model.OidType) + && !(own instanceof ErrorType) && !(inherited instanceof ErrorType)) { + var relation=extension(own,inherited,decl.language()); + if(relation.decision()==guru.interlis.model.RelationDecision.DISALLOWED) + diagnostics.emitRule(decl,"domain does not specialize its base: "+relation.reason(),"TYPE_EXTENSION"); + } + if (base.isPresent() && effectiveDescriptor(state.declaredType(base.orElseThrow())) instanceof guru.interlis.model.OidType oid + && (Optional.ofNullable(state.symbol(base.orElseThrow())) + .filter(s -> s.name().value().equals("INTERLIS.NOOID")).isPresent() + || oid.oidKind() != guru.interlis.model.OidKind.ANY + || !(effectiveDescriptor(type) instanceof guru.interlis.model.OidType derived) + || derived.oidKind() == guru.interlis.model.OidKind.ANY)) + diagnostics.emitRule(decl, "concrete OID domain cannot be extended", "OID_EXTENSION"); + return assigned; + } finally { + resolvingDomainTypes.remove(decl.id()); + } + } + + private EnumerationType effectiveEnumeration(TypeId id, EnumerationTypeSyntax syntax, + TypeDescriptor inherited, DeclInfo declaration) { + EnumerationType base = inherited instanceof EnumerationType e ? e : null; + if (inherited != null && (base == null || base.treeValues())) diagnostics.emitRule(declaration, "enumeration definition requires an enumeration definition as base", "ENUM_BASE"); + if (base != null && base.id().equals(id)) diagnostics.emitRule(declaration, "cyclic enumeration inheritance", "ENUM_BASE"); + if (base != null && base.circular()) diagnostics.emitRule(declaration, "circular enumeration cannot be extended", "ENUM_CIRCULAR"); + if (base != null && syntax.circular()) diagnostics.emitRule(declaration, "enumeration extension cannot introduce circular ordering", "ENUM_ORDER"); + Map values = new LinkedHashMap<>(); + if (base != null) base.values().forEach(v -> values.put(v.path(), v)); + Set own = new LinkedHashSet<>(); + Set finals = new LinkedHashSet<>(base == null ? Set.of() : base.finalSubtrees()); + for (var value : syntax.values()) { + work.checkpoint(); + String path = value.path(); + String parent = path.contains(".") ? path.substring(0, path.lastIndexOf('.')) : ""; + if (value.qualified() && (base == null || base.values().stream().noneMatch(v -> v.path().equals(path)))) + diagnostics.emitRule(declaration, "qualified enum name must identify an inherited element", "ENUM_PARENT"); + if (!own.add(path)) diagnostics.emitRule(declaration, "duplicate enumeration value " + path, "ENUM_VALUE_UNIQUE"); + if (values.containsKey(path)) { + if (!value.hasChildren()) diagnostics.emitRule(declaration, "extended enumeration repeats inherited value " + path, "ENUM_VALUE_UNIQUE"); + } else { + if (!parent.isEmpty() && !values.containsKey(parent)) diagnostics.emitRule(declaration, "unknown enumeration parent " + parent, "ENUM_PARENT"); + if (finals.contains(parent)) diagnostics.emitRule(declaration, "enumeration sibling list is FINAL: " + parent, "ENUM_FINAL"); + values.put(path, new EnumerationValue(path, 0, Optional.of(value.range()))); + } + } + finals.addAll(syntax.finalSubtrees()); + // Preserve inherited sibling order and place new descendants under their parent. + List ordered = new ArrayList<>(); + appendEnumerationChildren("", values, ordered); + return new EnumerationType(id, new Origin.DeclaredOrigin(syntax.range()), ordered, + syntax.ordered() || syntax.circular() || base != null && base.ordered(), syntax.circular(), finals, Optional.ofNullable(base).map(EnumerationType::id), false); + } + + private void appendEnumerationChildren(String parent, Map values, List result) { + for (var value : values.values()) { + work.checkpoint(); + String path = value.path(); + String candidate = path.contains(".") ? path.substring(0, path.lastIndexOf('.')) : ""; + if (candidate.equals(parent)) { + result.add(new EnumerationValue(path, result.size(), value.range())); + appendEnumerationChildren(path, values, result); + } + } + } + + boolean mandatoryDomain(SymbolId domain) { + Set seen = new HashSet<>(); + for (SymbolId current = domain; current != null && seen.add(current); current = state.baseOf(current)) { + work.checkpoint(); + if (index.byId(current).filter(d -> d.modifiers().contains(guru.interlis.model.Modifier.MANDATORY)).isPresent()) return true; + if (state.symbol(current) instanceof DomainSymbol d && d.mandatory()) return true; + } + return false; + } + + boolean mandatoryType(TypeId type) { + Set seen = new HashSet<>(); + while (type != null && seen.add(type) && state.type(type) instanceof DomainReferenceType reference) { + if (mandatoryDomain(reference.targetDomain())) return true; + type = state.declaredType(reference.targetDomain()); + } + return false; + } + + /** Complete only owned inline types; referenced alias descriptors are never mutated. */ + private void inheritTypeProperties(TypeId id, TypeSyntax syntax, TypeDescriptor base,DeclInfo declaration) { + if (syntax instanceof NumericTypeSyntax own && state.type(id) instanceof NumericType numeric && base instanceof NumericType parent) { + var range=inheritRange(numeric.range(),parent.range(),own);var errors=new ArrayList(); + units.validateNumericUnit(range,declaration,syntax.range(),errors); + state.replaceType(errors.isEmpty()?new NumericType(id,numeric.origin(),range):new ErrorType(id,numeric.origin(),errors)); + } + else if(syntax instanceof CoordinateTypeSyntax own && state.type(id) instanceof CoordinateType coordinate && base instanceof CoordinateType parent) { + List axes=new ArrayList<>(coordinate.axes()); + for(int i=0;i(); + for(int i=0;i effectiveDescriptor(state.declaredType(domain))).decision() == guru.interlis.model.RelationDecision.ALLOWED; + } + + /** Resolve once so every expression and the published attribute share the same type. */ + TypeId attributeType(DeclInfo decl) { + TypeId existing = state.declaredType(decl.id()); + if (existing != null) return existing; + work.checkpoint(); + AttributeDeclarationSyntax syntax = (AttributeDeclarationSyntax) decl.syntax(); + TypeId type; + if (!resolvingAttributeTypes.add(decl.id())) { + type = nextTypeId(); + state.defineType(new ErrorType(type, decl.origin(), + List.of(diagnostics.emitRule(decl, "cyclic inherited attribute type", "INHERITANCE_CYCLE")))); + return type; + } + try { + decl.owner().flatMap(inheritance::objectInheritanceBase).flatMap(base->paths.findMember(base,decl.name())) + .filter(base->state.symbol(base) instanceof AttributeSymbol || index.byId(base).filter(d->d.kind()==SymbolKind.ATTRIBUTE).isPresent()) + .ifPresent(base->inheritance.bindBase(decl.id(),base)); + if (syntax.type() instanceof GenericTypeSyntax inferred && "INFERRED".equals(inferred.name())) { + var result = expressions.get().buildExpression(syntax.derivations().getFirst(), decl.owner().orElseThrow()); + inferredDerivations.put(decl.id(), List.of(result.id())); + type = result.typeId(); + } else if (syntax.type() instanceof GenericTypeSyntax generic && "INHERITED".equals(generic.name())) { + Optional base = decl.owner().flatMap(inheritance::objectInheritanceBase) + .flatMap(owner -> paths.findMember(owner, decl.name())); + DeclInfo inherited = base.flatMap(index::byId).orElse(null); + if (inherited != null && inherited.kind() == SymbolKind.ATTRIBUTE) { + type = attributeType(inherited); + } else if (base.map(state::symbol).orElse(null) instanceof AttributeSymbol attribute) { + // ALL OF members have a bound type but no separate syntax declaration. + type = attribute.declaredType(); + } else { + type = nextTypeId(); + state.defineType(new ErrorType(type, decl.origin(), List.of(diagnostics.emitRule(decl, + "MANDATORY without a type requires an inherited attribute", "ATTRIBUTE_INHERITED_TYPE")))); + } + } else { + type = buildType(syntax.type(), decl.id(), decl.sourceId()); + TypeDescriptor inheritedGeometry = null; + if (decl.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + var base = decl.owner().flatMap(inheritance::objectInheritanceBase).flatMap(owner -> paths.findMember(owner, decl.name())); + TypeId baseType = base.flatMap(index::byId).filter(d -> d.kind() == SymbolKind.ATTRIBUTE) + .map(this::attributeType).orElseGet(() -> base.map(state::symbol) + .filter(AttributeSymbol.class::isInstance).map(AttributeSymbol.class::cast) + .map(AttributeSymbol::declaredType).orElse(null)); + inheritTypeProperties(type, syntax.type(), effectiveDescriptor(baseType),decl); + inheritedGeometry = effectiveDescriptor(baseType); + } + if (syntax.type() instanceof LineTypeSyntax) lineForms.effective(type, inheritedGeometry, decl); + } + state.bindDeclaredType(decl.id(), type); + return type; + } finally { + resolvingAttributeTypes.remove(decl.id()); + } + } + + /** Simple predefined domains allowed by DomainRef. No fallback for unknown names. */ + Optional builtinSimpleDomain(DeclInfo context, String name) { + if (!Set.of("INTERLIS.URI", "INTERLIS.NAME", "INTERLIS.BOOLEAN", "INTERLIS.INTERLIS_1_DATE", "INTERLIS.HALIGNMENT", "INTERLIS.VALIGNMENT").contains(name)) + return Optional.empty(); + SymbolId symbol = new SymbolId("predefined:" + context.language() + ":domain:" + name); + if (state.hasSymbol(symbol)) return Optional.of(symbol); + Origin origin = new Origin.PredefinedOrigin(context.language(), name); + TypeId type = nextTypeId(); + TypeDescriptor descriptor=switch(name) { + case "INTERLIS.URI" -> new TextType(type,origin,TextKind.URI,java.util.OptionalInt.of(1023)); + case "INTERLIS.NAME" -> new TextType(type,origin,TextKind.NAME,java.util.OptionalInt.of(255)); + case "INTERLIS.BOOLEAN" -> new guru.interlis.model.BooleanType(type,origin); + case "INTERLIS.INTERLIS_1_DATE" -> new TextType(type,origin,TextKind.TEXT,java.util.OptionalInt.of(8)); + default -> new guru.interlis.model.AlignmentType(type,origin,name.equals("INTERLIS.HALIGNMENT") + ? guru.interlis.model.AlignmentKind.HORIZONTAL : guru.interlis.model.AlignmentKind.VERTICAL); + }; + state.defineType(descriptor); + state.bindDeclaredType(symbol, type); + definePredefinedDomain.accept(new DomainSymbol(new SymbolHeader(symbol, SymbolKind.DOMAIN, + DeclaredName.synthetic(name), Optional.empty(), 0, origin, Optional.empty(), context.language(), + Documentation.empty(), MetaAttributeSet.empty(), Set.of(guru.interlis.model.Modifier.FINAL), List.of()), + type, Optional.empty(), false)); + return Optional.of(symbol); + } + + TypeId buildType(TypeSyntax syntax, SymbolId context, SourceId source) { + work.checkpoint(); + Objects.requireNonNull(syntax, "syntax"); + TypeId id = nextTypeId(); + TypeDescriptor descriptor = switch (syntax) { + case TextTypeSyntax text -> { + if (text.maxLength().isPresent() && text.maxLength().getAsInt() == 0) + yield new ErrorType(id, new Origin.DeclaredOrigin(text.range()), List.of(diagnostics.emitRule(index.byId(context).orElseThrow(), + "declared text maximum length must be positive", "TEXT_LENGTH", text.range()))); + yield new TextType(id, new Origin.DeclaredOrigin(text.range()), text.textKind(), + text.maxLength().isPresent() ? text.maxLength() : switch (text.textKind()) { + case NAME -> java.util.OptionalInt.of(255); + case URI -> java.util.OptionalInt.of(1023); + default -> java.util.OptionalInt.empty(); + }); + } + case BooleanTypeSyntax bool -> new guru.interlis.model.BooleanType(id, + new Origin.DeclaredOrigin(bool.range())); + case TemporalTypeSyntax syntaxTime -> { + String name = switch (syntaxTime.temporalKind()) { + case DATE -> "XMLDate"; case TIMEOFDAY -> "XMLTime"; case DATETIME -> "XMLDateTime"; + }; + var domain = temporal.find(index.byId(context).orElseThrow(), "INTERLIS." + name, Set.of(SymbolKind.DOMAIN)).orElseThrow(); + yield new DomainReferenceType(id, new Origin.DeclaredOrigin(syntaxTime.range()), domain); + } + case OidTypeSyntax oid -> { + Optional carrier = oid.valueType().map(t -> buildType(t, context, source)); + if (oid.oidKind() == guru.interlis.model.OidKind.UUID) { + TypeId text = nextTypeId(); state.defineType(new TextType(text, new Origin.DeclaredOrigin(oid.range()), TextKind.TEXT, java.util.OptionalInt.of(36))); + carrier = Optional.of(text); + } + yield new guru.interlis.model.OidType(id, new Origin.DeclaredOrigin(oid.range()), oid.oidKind(), carrier); + } + case BlackboxTypeSyntax blackbox -> new guru.interlis.model.BlackboxType(id, + new Origin.DeclaredOrigin(blackbox.range()), blackbox.blackboxKind()); + case AlignmentTypeSyntax alignment -> new guru.interlis.model.AlignmentType(id, + new Origin.DeclaredOrigin(alignment.range()), alignment.alignment()); + case EnumTreeValueSyntax enumTree -> { + List errors = new ArrayList<>(); + Optional domain = names.bindTarget(index.byId(context).orElseThrow(), enumTree.target(), + Set.of(SymbolKind.DOMAIN), ReferenceUsage.TYPE_USE, errors); + if (domain.isEmpty()) yield new ErrorType(id, new Origin.DeclaredOrigin(enumTree.range()), errors); + index.byId(domain.orElseThrow()).ifPresent(this::domainType); + TypeDescriptor base=effectiveDescriptor(state.declaredType(domain.orElseThrow())); + if(!(base instanceof EnumerationType enumeration) || enumeration.treeValues()) + yield new ErrorType(id,new Origin.DeclaredOrigin(enumTree.range()),List.of(diagnostics.emitRule(index.byId(context).orElseThrow(),"ALL OF requires an enumeration domain","ENUM_TREE_TARGET",enumTree.range()))); + yield new EnumerationType(id,new Origin.DeclaredOrigin(enumTree.range()),enumeration.values(),enumeration.ordered(), + enumeration.circular(),enumeration.finalSubtrees(),Optional.of(enumeration.id()),true); + } + case guru.interlis.syntax.EnumArgumentTypeSyntax argument -> + new guru.interlis.model.EnumArgumentType(id,new Origin.DeclaredOrigin(argument.range()),argument.treeValues()); + + case guru.interlis.syntax.StructureValueTypeSyntax structure -> structures.bind(id,structure,index.byId(context).orElseThrow()); + case GenericTypeSyntax generic -> { + if ("INHERITED".equals(generic.name())) { + yield new ErrorType(id, new Origin.DeclaredOrigin(generic.range()), List.of(diagnostics.emitRule( + index.byId(context).orElseThrow(), "a type can only be omitted on an inherited attribute", + "INHERITED_TYPE_CONTEXT"))); + } + yield new GenericType(id, new Origin.DeclaredOrigin(generic.range()), generic.name()); + } + case guru.interlis.syntax.AttributePathTypeSyntax path -> { + Optional owner = Optional.empty(); + if (path.argument().isPresent()) { + DeclInfo declaration = index.byId(context).orElseThrow(); + String name = path.argument().orElseThrow().text(); + int position = -1; + if (declaration.syntax() instanceof FunctionDeclarationSyntax function) { + for (int i = 0; i < function.parameters().size(); i++) + if (function.parameters().get(i).name().equals(name)) { position = i; break; } + } + if (position < 0) { + yield new ErrorType(id, new Origin.DeclaredOrigin(path.range()), List.of(diagnostics.emitRule(declaration, + "ATTRIBUTE OF @ requires an argument of this function", "ATTRIBUTE_ARGUMENT"))); + } + TypeDescriptor argumentType = effectiveDescriptor(functionArgumentType(declaration, position)); + if (!(argumentType instanceof guru.interlis.model.ObjectArgumentType || argumentType instanceof StructureValueType + || argumentType instanceof ClassValueType || argumentType instanceof guru.interlis.model.AttributePathType || argumentType instanceof ErrorType)) + diagnostics.emitRule(declaration, "ATTRIBUTE OF @ argument must describe objects, classes or an attribute path", "ATTRIBUTE_ARGUMENT_TYPE"); + owner = Optional.of(new guru.interlis.model.AttributePathType.Argument(context, position)); + } + Optional ownerClass = path.ownerClass().map(expression -> expressions.get().buildExpression(expression, index.byId(context).orElseThrow().owner().orElse(context))); + List restrictions = path.restrictions().stream().map(t -> buildType(t, context, source)).toList(); + if (ownerClass.isPresent() && !(effectiveDescriptor(ownerClass.orElseThrow().typeId()) instanceof ClassValueType)) + diagnostics.emitRule(index.byId(context).orElseThrow(), "ATTRIBUTE OF requires a class-valued attribute path", "ATTRIBUTE_PATH_TYPE"); + yield new guru.interlis.model.AttributePathType(id, new Origin.DeclaredOrigin(path.range()), + ownerClass.isPresent() ? Optional.of(new guru.interlis.model.AttributePathType.ClassAttribute(ownerClass.orElseThrow().id())) : owner, restrictions); + } + case ClassValueTypeSyntax classValue -> { + List errors = new ArrayList<>(); + List restrictions = new ArrayList<>(); + var kinds = classValue.structureValue() ? Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE) + : Set.of(SymbolKind.CLASS, SymbolKind.ASSOCIATION, SymbolKind.VIEW); + for (var name : classValue.restrictions()) { + names.bindTarget(index.byId(context).orElseThrow(), name, kinds, ReferenceUsage.TYPE_USE, errors).ifPresent(target -> { + SymbolId current = target; + Set visited = new HashSet<>(); + while (current != null && visited.add(current)) { work.checkpoint(); current = inheritance.objectInheritanceBase(current).orElse(null); } + restrictions.add(target); + }); + } + yield errors.isEmpty() ? new ClassValueType(id, new Origin.DeclaredOrigin(classValue.range()), classValue.structureValue(), restrictions) + : new ErrorType(id, new Origin.DeclaredOrigin(classValue.range()), errors); + } + case guru.interlis.syntax.ObjectArgumentTypeSyntax object -> + buildObjectArgumentType(id, object, context); + case guru.interlis.syntax.MetaObjectTypeSyntax meta -> { + var declaration = index.byId(context).orElseThrow(); + var target = meta.target().flatMap(name -> { + var predefined = metadata.predefined(declaration, name.text(), Set.of(SymbolKind.CLASS)); + if (predefined.isPresent()) references.recordReference(declaration, name, predefined.get(), ReferenceUsage.TYPE_USE); + return predefined.isPresent() ? predefined : names.bindTarget(declaration, name, Set.of(SymbolKind.CLASS), ReferenceUsage.TYPE_USE, new ArrayList<>()); + }); + target.ifPresent(idTarget -> { + metadata.ensureBases(idTarget); + var root = metadata.predefined(declaration, "INTERLIS.METAOBJECT", Set.of(SymbolKind.CLASS)).orElseThrow(); + if (!guru.interlis.model.internal.TypeCompatibility.specializes(idTarget, root, state::baseOf)) + diagnostics.emitRule(declaration, "METAOBJECT OF requires a metadata class", "PARAMETER_METAOBJECT_TARGET"); + }); + yield new guru.interlis.model.MetaObjectType(id, new Origin.DeclaredOrigin(meta.range()), target); + } + case LineTypeSyntax line -> lineForms.buildType(id, line, index.byId(context).orElseThrow()); + case FormattedTypeSyntax formatted -> formats.buildFormattedType(id, formatted, context); + case guru.interlis.syntax.NumericTypeSyntax numeric -> { + var invalid = new ArrayList<>(units.numericErrors(numeric, index.byId(context).orElseThrow())); + if (!invalid.isEmpty()) yield new ErrorType(id, new Origin.DeclaredOrigin(numeric.range()), invalid); + var range=metadata.numeric(numeric,index.byId(context).orElseThrow(),invalid); + units.validateNumericUnit(range,index.byId(context).orElseThrow(),numeric.range(),invalid); + yield invalid.isEmpty()?new NumericType(id,new Origin.DeclaredOrigin(numeric.range()),range) + :new ErrorType(id,new Origin.DeclaredOrigin(numeric.range()),invalid); + } + case EnumerationTypeSyntax enumeration -> { + DeclInfo declaration = index.byId(context).orElseThrow(); + TypeDescriptor inherited = null; + if (declaration.kind() == SymbolKind.ATTRIBUTE && declaration.modifiers().contains(guru.interlis.model.Modifier.EXTENDED)) { + SymbolId owner = declaration.owner().orElse(null); + AttributeSymbol baseAttribute = owner == null ? null : inheritance.findBaseAttribute(state.baseOf(owner), declaration.name()); + if (baseAttribute != null) inherited = effectiveDescriptor(baseAttribute.declaredType()); + } + // Domains are merged after their base reference has been bound. + yield declaration.kind() == SymbolKind.DOMAIN + ? new EnumerationType(id, new Origin.DeclaredOrigin(enumeration.range()), List.of(), + enumeration.ordered(), enumeration.circular(), enumeration.finalSubtrees(), Optional.empty(), false) + : effectiveEnumeration(id, enumeration, inherited, declaration); + } + case DomainReferenceSyntax reference -> { + var declaration=index.byId(context).orElseThrow();var errors=new ArrayList(); + var target=names.bindTarget(declaration,reference.name(),Set.of(SymbolKind.DOMAIN,SymbolKind.STRUCTURE),ReferenceUsage.TYPE_USE,errors); + if(target.isEmpty())yield new ErrorType(id,new Origin.DeclaredOrigin(reference.range()),errors); + SymbolId selected=target.orElseThrow(); + var sourceDeclaration=index.byId(selected); + SymbolKind kind=sourceDeclaration.map(DeclInfo::kind).orElseGet(()->state.symbol(selected).kind()); + if(kind==SymbolKind.STRUCTURE)yield new StructureValueType(id,new Origin.DeclaredOrigin(reference.range()),Optional.of(selected),List.of()); + sourceDeclaration.ifPresent(this::domainType); + yield new DomainReferenceType(id,new Origin.DeclaredOrigin(reference.range()),selected); + } + case ReferenceTypeSyntax reference -> { + DeclInfo declaration = index.byId(context).orElseThrow(); + Optional target = reference.target().text().equals("ANYCLASS") ? Optional.empty() + : Optional.of(declarations.get().bindObjectTarget(declaration, reference.target(), ReferenceUsage.TYPE_USE)); + List restrictions = reference.restrictions().stream() + .map(name -> declarations.get().bindObjectTarget(declaration, name, ReferenceUsage.TYPE_USE)).toList(); + for (SymbolId restriction : restrictions) { + work.checkpoint(); + if (target.isPresent() && !inheritance.targetSpecializes(restriction, target.orElseThrow())) + diagnostics.emitRule(declaration, "reference restriction must specialize its primary target", "REFERENCE_TARGET_RESTRICTION"); + } + yield new ReferenceType(id, new Origin.DeclaredOrigin(reference.range()), target, restrictions, reference.external()); + } + case CollectionTypeSyntax collection -> { + TypeId element = buildType(collection.elementType(), context, source); + if (index.byId(context).orElseThrow().language() == guru.interlis.model.LanguageVersion.ILI23 + && !(effectiveDescriptor(element) instanceof StructureValueType) + && !(effectiveDescriptor(element) instanceof ErrorType)) + diagnostics.emitRule(index.byId(context).orElseThrow(), "INTERLIS 2.3 collections require structure elements", "COLLECTION_ELEMENT_VERSION"); + yield new CollectionType(id, new Origin.DeclaredOrigin(collection.range()), + collection.collectionKind(), collection.cardinality(), element); + } + case CoordinateTypeSyntax coordinate -> { + List invalid = new ArrayList<>(); + for (var axis : coordinate.axes()) invalid.addAll(units.numericErrors(axis, index.byId(context).orElseThrow())); + if (!invalid.isEmpty()) yield new ErrorType(id, new Origin.DeclaredOrigin(coordinate.range()), invalid); + var axes=new ArrayList(); + for(var axis:coordinate.axes()) { + var range=metadata.numeric(axis,index.byId(context).orElseThrow(),invalid); + units.validateNumericUnit(range,index.byId(context).orElseThrow(),axis.range(),invalid);axes.add(range); + } + yield invalid.isEmpty()?new guru.interlis.model.CoordinateType(id, + new Origin.DeclaredOrigin(coordinate.range()),coordinate.dimensions(),axes,coordinate.multiCoordinate(),coordinate.referenceSystem(),coordinate.rotation()) + :new ErrorType(id,new Origin.DeclaredOrigin(coordinate.range()),invalid); + } + case guru.interlis.syntax.UnsupportedTypeSyntax unsupported -> { + if (unsupported.featureId().equals("MISSING_ATTRIBUTE_TYPE") || unsupported.featureId().equals("MISSING_FUNCTION_RETURN_TYPE")) { + DiagnosticId cause = null; + int distance = Integer.MAX_VALUE; + for (var diagnostic : diagnostics.syntaxErrors()) { + work.checkpoint(); + var range = diagnostic.primaryRange().orElse(null); + if (range == null || !range.source().equals(unsupported.range().source()) + || !range.revision().equals(unsupported.range().revision())) continue; + int candidate = Math.abs(range.startOffset() - unsupported.range().endOffset()); + if (candidate < distance) { cause = diagnostic.id(); distance = candidate; } + } + if (cause == null) cause = diagnostics.emitRule(index.byId(context).orElseThrow(), "attribute type is missing", "SOURCE_MISSING_TYPE", unsupported.range()); + yield new ErrorType(id, new Origin.DeclaredOrigin(unsupported.range()), List.of(cause)); + } + DiagnosticId diagnostic = diagnostics.emitUnsupported(source, unsupported.range(), + unsupported.featureId(), unsupported.construct()); + yield new ErrorType(id, new Origin.DeclaredOrigin(unsupported.range()), + List.of(diagnostic)); + } + }; + state.defineType(descriptor); + return id; + } + + private TypeDescriptor buildObjectArgumentType(TypeId id, + guru.interlis.syntax.ObjectArgumentTypeSyntax syntax, SymbolId context) { + DeclInfo owner = index.byId(context).orElseThrow(); + List errors = new ArrayList<>(); + List restrictions = new ArrayList<>(); + Optional base = syntax.base().flatMap(name -> + declarations.get().bindObjectTarget(owner, name, errors)); + for (var name : syntax.restrictions()) { + work.checkpoint(); + Optional restriction = declarations.get().bindObjectTarget(owner, name, errors); + restriction.ifPresent(restrictions::add); + if (base.isPresent() && restriction.isPresent() + && !guru.interlis.model.internal.TypeCompatibility.specializes( + restriction.get(), base.get(), state::baseOf)) { + errors.add(diagnostics.emit(DiagnosticDraft.of(new DiagnosticCode("ILI-SEM-0005"), + Severity.ERROR, Phase.TYPE_CHECKING, "diagnostic.semantics.type") + .at(name.range()) + .argument(new DiagnosticArgument.TypeNameArgument("actual", name.text())) + .argument(new DiagnosticArgument.TypeNameArgument("expected", + "specialization of " + syntax.base().orElseThrow().text())))); + } + } + Origin origin = new Origin.DeclaredOrigin(syntax.range()); + return errors.isEmpty() + ? new guru.interlis.model.ObjectArgumentType(id, origin, base, restrictions, syntax.objects()) + : new ErrorType(id, origin, errors); + } + + TypeId errorType(SourceId source, SourceRange range, String feature) { + TypeId id = nextTypeId(); + DiagnosticId diagnostic = diagnostics.emitUnsupported(source, range, feature, feature); + state.defineType(new ErrorType(id, new Origin.SyntheticOrigin( + guru.interlis.model.SyntheticReason.MISSING_TYPE, Optional.of(range)), + List.of(diagnostic))); + return id; + } + + TypeId builtinBoolean(guru.interlis.model.LanguageVersion language) { + return booleanTypes.computeIfAbsent(language,version->{ + TypeId id=nextTypeId();state.defineType(new guru.interlis.model.BooleanType(id,new Origin.PredefinedOrigin(version,"BOOLEAN")));return id; + }); + } + + TypeId nextTypeId() { + return state.nextTypeId(); + } + + boolean isBoolean(TypeId typeId) { + var type=effectiveDescriptor(typeId); + return type instanceof guru.interlis.model.BooleanType || guru.interlis.model.internal.TypeCompatibility.isUndefined(type); + } + + boolean isNumeric(TypeId typeId) { + var type=effectiveDescriptor(typeId); + return type instanceof NumericType || guru.interlis.model.internal.TypeCompatibility.isUndefined(type); + } + + TypeDescriptor effectiveDescriptor(TypeId typeId) { + TypeDescriptor descriptor = state.type(typeId); + Set seen = new HashSet<>(); + while (descriptor instanceof DomainReferenceType reference && seen.add(descriptor.id())) { + work.checkpoint(); + TypeId target = state.declaredType(reference.targetDomain()); + if (target == null) break; + descriptor = state.type(target); + } + return descriptor; + } + + String typeName(TypeId id) { + TypeDescriptor descriptor = effectiveDescriptor(id); + if(descriptor instanceof guru.interlis.model.EnumArgumentType argument)return argument.treeValues()?"ENUMTREEVAL":"ENUMVAL"; + if (descriptor instanceof TextType text) { + return text.textKind().name() + (text.maxLength().isPresent() ? "*" + text.maxLength().getAsInt() : ""); + } + return descriptor == null ? "UNKNOWN" : descriptor.kind().name(); + } + + void buildEffectiveTypes() { + for (Map.Entry entry : state.declaredTypesInBuildOrder()) { + work.checkpoint(); + state.setEffectiveType(entry.getKey(), effectiveType(entry.getValue())); + } + } + + private EffectiveType effectiveType(TypeId declared) { + List aliasPath = new ArrayList<>(); + TypeId current = declared; + Set seen = new HashSet<>(); + while (true) { + work.checkpoint(); + TypeDescriptor descriptor = state.type(current); + if (!(descriptor instanceof DomainReferenceType reference)) { + break; + } + if (!seen.add(current)) { + break; + } + aliasPath.add(reference.targetDomain()); + Symbol symbol = state.symbol(reference.targetDomain()); + if (symbol instanceof DomainSymbol domain) { + current = domain.typeId(); + } else { + break; + } + } + return new EffectiveType(declared, current, aliasPath); + } + + List inferredDerivations(SymbolId declaration) { + return inferredDerivations.get(declaration); + } + + void defineType(TypeDescriptor type) { state.defineType(type); } + + void replaceType(TypeDescriptor type) { state.replaceType(type); } + + void bindDeclaredType(SymbolId symbol, TypeId type) { state.bindDeclaredType(symbol, type); } + + int nextTypeSequence() { return state.nextTypeSequence(); } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/UnitBindings.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/UnitBindings.java new file mode 100644 index 00000000..9693f7ae --- /dev/null +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/UnitBindings.java @@ -0,0 +1,268 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.diagnostics.DiagnosticId; +import guru.interlis.model.CollectionType; +import guru.interlis.model.CoordinateType; +import guru.interlis.model.DeclaredName; +import guru.interlis.model.Documentation; +import guru.interlis.model.DomainReferenceType; +import guru.interlis.model.DomainSymbol; +import guru.interlis.model.MetaAttributeSet; +import guru.interlis.model.NumericRange; +import guru.interlis.model.NumericType; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolHeader; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TypeDescriptor; +import guru.interlis.model.TypeId; +import guru.interlis.model.UnitSymbol; +import guru.interlis.source.SourceRange; +import guru.interlis.syntax.AttributeDeclarationSyntax; +import guru.interlis.syntax.GenericTypeSyntax; +import guru.interlis.syntax.NumericTypeSyntax; +import guru.interlis.syntax.UnitDeclarationSyntax; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Builds and checks the bound unit graph and numeric unit requirements. */ +final class UnitBindings { + private final SemanticGraphRead state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameBindings names; + private final Supplier temporal; + private final Consumer definePredefinedUnit; + private final Function symbolHeader; + private final Function ensureSymbol; + private final Function effectiveDescriptor; + + UnitBindings(SemanticGraphRead state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameBindings names, + Supplier temporal, + Consumer definePredefinedUnit, + Function symbolHeader, + Function ensureSymbol, + Function effectiveDescriptor) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.names = names; + this.temporal = temporal; + this.definePredefinedUnit = definePredefinedUnit; + this.symbolHeader = symbolHeader; + this.ensureSymbol = ensureSymbol; + this.effectiveDescriptor = effectiveDescriptor; + } + + UnitSymbol buildUnit(DeclInfo decl) { + UnitDeclarationSyntax syntax = (UnitDeclarationSyntax) decl.syntax(); + Optional base = syntax.base().flatMap(name -> bindUnit(decl, name)); + Optional derived = syntax.derivedFrom().flatMap(name -> bindUnit(decl, name)); + List components = new ArrayList<>(); + for (var component : syntax.components()) { + work.checkpoint(); + bindUnit(decl, component.unit()).ifPresent(id -> components.add(new UnitSymbol.Component(component.operator(), id))); + } + return new UnitSymbol(symbolHeader.apply(decl), syntax.designation(), syntax.abstractUnit(), base, derived, + components, syntax.conversionFactors(), syntax.conversionFunction(), syntax.definition().isBlank() ? Optional.empty() : Optional.of(syntax.definition())); + } + + Optional bindUnit(DeclInfo decl, guru.interlis.syntax.NameSyntax name) { + return bindUnit(decl,name,new ArrayList<>()); + } + + Optional bindUnit(DeclInfo decl, guru.interlis.syntax.NameSyntax name, List errors) { + return names.bindTarget(decl,name,Set.of(SymbolKind.UNIT),ReferenceUsage.TYPE_USE,errors,false); + } + + Optional builtinUnit(SymbolId context, String name) { + if (!name.startsWith("INTERLIS.")) return Optional.empty(); + var timeUnit = temporal.get().find(index.byId(context).orElseThrow(), name, Set.of(SymbolKind.UNIT)); + if (timeUnit.isPresent()) return timeUnit; + String shortName = name.substring("INTERLIS.".length()); + String parent = switch (shortName) { + case "ANYUNIT" -> ""; + case "DIMENSIONLESS", "LENGTH", "MASS", "TIME", "ELECTRIC_CURRENT", "TEMPERATURE", + "AMOUNT_OF_MATTER", "ANGLE", "SOLID_ANGLE", "LUMINOUS_INTENSITY", "MONEY" -> "ANYUNIT"; + case "m" -> "LENGTH"; case "kg" -> "MASS"; case "s" -> "TIME"; + case "A" -> "ELECTRIC_CURRENT"; case "K" -> "TEMPERATURE"; case "mol" -> "AMOUNT_OF_MATTER"; + case "rad" -> "ANGLE"; case "sr" -> "SOLID_ANGLE"; case "cd" -> "LUMINOUS_INTENSITY"; + default -> null; + }; + if (parent == null) return Optional.empty(); + var language = index.byId(context).orElseThrow().language(); + SymbolId id = new SymbolId("predefined:" + language + ":unit:" + name); + if (state.hasSymbol(id)) return Optional.of(id); + Optional base = parent.isEmpty() ? Optional.empty() : builtinUnit(context, "INTERLIS." + parent); + boolean abstractUnit = parent.isEmpty() || parent.equals("ANYUNIT"); + String designation = switch (shortName) { + case "m" -> "METER"; case "kg" -> language==guru.interlis.model.LanguageVersion.ILI23?"KILOGRAMM":"KILOGRAM"; case "s" -> "SECOND"; + case "A" -> "AMPERE"; case "K" -> "DEGREE_KELVIN"; case "mol" -> "MOLE"; + case "rad" -> "RADIAN"; case "sr" -> "STERADIAN"; case "cd" -> "CANDELA"; + default -> shortName; + }; + Origin origin = new Origin.PredefinedOrigin(language, name); + definePredefinedUnit.accept(new UnitSymbol(new SymbolHeader(id, SymbolKind.UNIT, DeclaredName.synthetic(name), + Optional.empty(), 0, origin, Optional.empty(), language, + Documentation.empty(), MetaAttributeSet.empty(), abstractUnit + ? Set.of(guru.interlis.model.Modifier.ABSTRACT) : Set.of(), List.of()), + designation, abstractUnit, base, Optional.empty(), List.of(), List.of(), Optional.empty(), Optional.empty())); + return Optional.of(id); + } + + /** Validate bound dependencies after all declarations exist; file order is immaterial. */ + void validateUnits() { + for (Symbol symbol : state.symbolsInBuildOrder()) { + if (!(symbol instanceof UnitSymbol unit)) continue; + DeclInfo decl = index.byId(unit.id()).orElse(null); + if (decl == null) continue; + work.checkpoint(); + var syntax = (UnitDeclarationSyntax) decl.syntax(); + for (var factor : unit.conversionFactors()) { + work.checkpoint(); + if (factor.decimal().filter(value -> value.exactValue().signum() == 0).isPresent()) { + boolean divide = factor.operator() == UnitSymbol.Operator.DIVIDE; + diagnostics.emitRule(decl, divide ? "unit conversion divides by zero" : "unit conversion multiplies by zero", + divide ? "UNIT_CONVERSION_ZERO_DIVISOR" : "UNIT_CONVERSION_ZERO_FACTOR", factor.range()); + } + } + if (index.children(decl.owner().orElseThrow()).stream() + .anyMatch(other -> !other.id().equals(decl.id()) && other.name().equals(decl.name()))) + diagnostics.emitRule(decl, "unit short name is not unique in its scope", "UNIT_NAME_UNIQUE"); + var pending = new java.util.ArrayDeque(unitDependencies(unit)); + Set seen = new HashSet<>(); + while (!pending.isEmpty()) { + work.checkpoint(); + SymbolId next = pending.removeFirst(); + if (next.equals(unit.id())) { + diagnostics.emitRule(decl, "cyclic unit dependency", "UNIT_CYCLE"); break; + } + if (seen.add(next) && state.symbol(next) instanceof UnitSymbol dependency) + pending.addAll(unitDependencies(dependency)); + } + for (var component : unit.components()) { + UnitSymbol target = (UnitSymbol) state.symbol(component.unit()); + if (target.abstractUnit() != unit.abstractUnit()) + diagnostics.emitRule(decl, "composed unit and all components must agree on abstraction", "UNIT_COMPONENT_ABSTRACT"); + } + if (unit.derivedFrom().isPresent()) { + UnitSymbol target = (UnitSymbol) state.symbol(unit.derivedFrom().orElseThrow()); + if (unit.abstractUnit() || target.abstractUnit()) + diagnostics.emitRule(decl, "derived units require a concrete conversion target and are concrete", "UNIT_DERIVED_ABSTRACT"); + } + UnitSymbol base = unit.base().map(state::symbol).filter(UnitSymbol.class::isInstance).map(UnitSymbol.class::cast).orElse(null); + if (base == null) continue; + if (!base.abstractUnit()) diagnostics.emitRule(decl, "only abstract units can be extended", "UNIT_BASE_ABSTRACT"); + if (!syntax.components().isEmpty()) { + boolean compatible = !base.components().isEmpty() && base.components().size() == unit.components().size(); + if (compatible) for (int i = 0; i < unit.components().size(); i++) { + var actual = unit.components().get(i); var expected = base.components().get(i); + if (actual.operator() != expected.operator() || !unitExtends(actual.unit(), expected.unit())) compatible = false; + } + if (!compatible) diagnostics.emitRule(decl, "composition must specialize the base component by component", "UNIT_COMPOSITION_EXTENSION"); + } else if (unit.derivedFrom().isPresent() && !unitExtends(unit.derivedFrom().orElseThrow(), base.id())) { + diagnostics.emitRule(decl, "conversion target must specialize the declared abstract base", "UNIT_DERIVED_EXTENSION"); + } + } + } + + private List unitDependencies(UnitSymbol unit) { + List result = new ArrayList<>(); + unit.base().ifPresent(result::add); unit.derivedFrom().ifPresent(result::add); + unit.components().forEach(c -> result.add(c.unit())); return result; + } + + boolean unitExtends(SymbolId actual, SymbolId expected) { + UnitSymbol target = extensionUnit(expected); + if (target == null) return false; + if (target.name().value().equals("INTERLIS.ANYUNIT")) return true; + var pending = new java.util.ArrayDeque(); pending.add(actual); + Set seen = new HashSet<>(); + while (!pending.isEmpty()) { + work.checkpoint(); SymbolId next = pending.removeFirst(); + if (next.equals(expected)) return true; + if (!seen.add(next)) continue; + UnitSymbol unit = extensionUnit(next); + if (unit == null) continue; + if (unit.header().origin() instanceof Origin.SyntheticOrigin + && target.header().origin() instanceof Origin.SyntheticOrigin && unit.name().equals(target.name())) return true; + unit.base().ifPresent(pending::add); unit.derivedFrom().ifPresent(pending::add); + } + return false; + } + + boolean unitsConvertible(SymbolId actual, SymbolId expected) { + return guru.interlis.model.internal.TypeCompatibility.unitRelation(actual, expected, this::extensionUnit) + .filter(relation -> relation.decision() == guru.interlis.model.RelationDecision.ALLOWED).isPresent(); + } + + UnitSymbol extensionUnit(SymbolId id) { + work.checkpoint(); + Symbol symbol = state.symbol(id); + if (symbol == null) symbol = index.byId(id).map(ensureSymbol::apply).orElse(null); + return symbol instanceof UnitSymbol unit ? unit : null; + } + + guru.interlis.model.TypeRelation numericExtension(NumericType actual, NumericType base, guru.interlis.model.LanguageVersion language) { + return guru.interlis.model.internal.TypeCompatibility.numericExtension(actual, base, language, this::extensionUnit); + } + + List numericErrors(NumericTypeSyntax syntax, DeclInfo declaration) { + List errors = new ArrayList<>(); + if (syntax.minimum().isPresent() && syntax.maximum().isPresent()) { + var minimum = syntax.minimum().orElseThrow(); var maximum = syntax.maximum().orElseThrow(); + if (minimum.exactValue().compareTo(maximum.exactValue()) > 0) + errors.add(diagnostics.emitRule(declaration, "numeric minimum exceeds maximum", "NUMERIC_BOUNDS", syntax.range())); + if (minimum.precision().fractionalDigits() != maximum.precision().fractionalDigits()) + errors.add(diagnostics.emitRule(declaration, "numeric bounds must have the same precision", "NUMERIC_PRECISION", syntax.range())); + } + return List.copyOf(errors); + } + + void validateNumericUnit(NumericRange range,DeclInfo declaration,SourceRange sourceRange,List errors) { + if(range.minimum().isPresent() && range.maximum().isPresent() && range.unit().isPresent()) { + UnitSymbol unit=extensionUnit(range.unit().orElseThrow()); + if(unit!=null && unit.abstractUnit())errors.add(diagnostics.emitRule(declaration,"concrete numeric range requires a concrete unit","NUMERIC_ABSTRACT_UNIT",sourceRange)); + } + } + + void validateNumericUses() { + for(var entry:state.declaredTypesInBuildOrder()) { + work.checkpoint();var declaration=index.byId(entry.getKey()).orElse(null); + if(declaration==null || declaration.kind()!=SymbolKind.DOMAIN && declaration.kind()!=SymbolKind.ATTRIBUTE)continue; + var type=effectiveDescriptor.apply(entry.getValue()); + while(type instanceof CollectionType collection)type=effectiveDescriptor.apply(collection.elementType()); + boolean incomplete=type instanceof NumericType number && (number.range().minimum().isEmpty() || number.range().maximum().isEmpty()) + || type instanceof CoordinateType coordinate && coordinate.axes().stream().anyMatch(axis->axis.minimum().isEmpty() || axis.maximum().isEmpty()); + if(!incomplete || declaration.modifiers().contains(guru.interlis.model.Modifier.ABSTRACT))continue; + if(declaration.syntax() instanceof AttributeDeclarationSyntax attribute && attribute.type() instanceof GenericTypeSyntax inferred && inferred.name().equals("INFERRED"))continue; + boolean generic=declaration.language()==guru.interlis.model.LanguageVersion.ILI24 && type instanceof CoordinateType + && (declaration.modifiers().contains(guru.interlis.model.Modifier.GENERIC) || genericCoordinateType(entry.getValue(),new HashSet<>())); + if(!generic)diagnostics.emitRule(declaration,"incomplete numeric or coordinate declaration requires ABSTRACT","NUMERIC_ABSTRACT"); + } + } + + private boolean genericCoordinateType(TypeId id,Set seen) { + if(!seen.add(id))return false; + if(state.type(id) instanceof DomainReferenceType reference) { + var domain=state.symbol(reference.targetDomain()); + return domain instanceof DomainSymbol d && (d.generic() || genericCoordinateType(d.typeId(),seen)); + } + return false; + } +} diff --git a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ViewBuilder.java b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ViewBuilder.java index 5e1ff51d..23992f1a 100644 --- a/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ViewBuilder.java +++ b/interlis-compiler/src/main/java/guru/interlis/compiler/internal/ViewBuilder.java @@ -2,11 +2,27 @@ import guru.interlis.model.*; import guru.interlis.syntax.*; + import java.util.*; +import java.util.function.Function; -/** View construction within one GraphBuilder run; reuses its type, path and reference binders. */ +/** View construction within one run using the shared type, path and reference binders. */ final class ViewBuilder { - private final GraphBuilder g; + private final SemanticGraphRead.Symbols state; + private final SemanticWork work; + private final IndexedDeclarations index; + private final SemanticDiagnostics diagnostics; + private final NameResolver resolver; + private final ReferenceBindings references; + private final InheritanceBindings inheritance; + private final ExpressionBindings expressions; + private final TypeBindings typeBindings; + private final ObjectPathBindings paths; + private final StructureBindings structures; + private final TopicBindings topics; + private final LineForms lineForms; + private final Function ensureSymbol; + private final Function symbolHeader; private final Map segmentContexts = new HashMap<>(); /** Semantic roles of wrappers created below; these are not source names. */ enum GeometryElementRole { LINE_GEOMETRY, SURFACE_EDGE, SURFACE_BOUNDARY } @@ -25,7 +41,7 @@ private void registerGeometryElement(SymbolId symbol, GeometryType geometry, SegmentContextualizer segments, GeometryElementRole role) { var binding = new GeometryContext(geometry, segments); geometryContexts.put(symbol, binding); - var structure = (StructureSymbol)g.symbols.get(symbol); + var structure = (StructureSymbol)state.symbol(symbol); for (SymbolId member : structure.members()) geometryContexts.put(member, binding); for (SymbolId constraint : structure.constraints()) geometryContexts.put(constraint, binding); for (SymbolId copied : segments.contextualSymbols().values()) geometryContexts.put(copied, binding); @@ -37,47 +53,81 @@ private void registerGeometryElement(SymbolId symbol, GeometryType geometry, private final Map> factorSources = new LinkedHashMap<>(); private final Set building = new HashSet<>(); private final Map> members = new LinkedHashMap<>(); - ViewBuilder(GraphBuilder graph) { g = graph; } + + ViewBuilder(SemanticGraphRead.Symbols state, + SemanticWork work, + IndexedDeclarations index, + SemanticDiagnostics diagnostics, + NameResolver resolver, + ReferenceBindings references, + InheritanceBindings inheritance, + ExpressionBindings expressions, + TypeBindings typeBindings, + ObjectPathBindings paths, + StructureBindings structures, + TopicBindings topics, + LineForms lineForms, + Function ensureSymbol, + Function symbolHeader) { + this.state = state; + this.work = work; + this.index = index; + this.diagnostics = diagnostics; + this.resolver = resolver; + this.references = references; + this.inheritance = inheritance; + this.expressions = expressions; + this.typeBindings = typeBindings; + this.paths = paths; + this.structures = structures; + this.topics = topics; + this.lineForms = lineForms; + this.ensureSymbol = ensureSymbol; + this.symbolHeader = symbolHeader; + } void contextualMembers(SymbolId owner, List attributes) { members.put(owner, List.copyOf(attributes)); } - ViewSymbol.Base projectionBase(SymbolId view) { return (ViewSymbol.Base)g.symbols.get(projections.get(view)); } + ViewSymbol.Base projectionBase(SymbolId view) { return (ViewSymbol.Base)state.symbol(projections.get(view)); } ViewSymbol.Inspection inspection(SymbolId view) { return inspections.get(view); } SymbolId basisTarget(ViewSymbol.Base base) { return inspectedTargets.getOrDefault(base.id(), base.target()); } Optional member(SymbolId owner, String name) { return members.getOrDefault(owner, List.of()).stream() - .filter(id -> g.symbols.get(id).name().value().equals(name)).findFirst(); + .filter(id -> state.symbol(id).name().value().equals(name)).findFirst(); } Symbol buildBase(DeclInfo decl) { NameSyntax targetName = (NameSyntax) decl.syntax(); - var resolution = g.resolver.resolve(decl.id(), targetName, + var resolution = resolver.resolve(decl.id(), targetName, Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE, SymbolKind.ASSOCIATION, SymbolKind.VIEW), false); if (resolution.target().isEmpty()) { - g.unresolved(targetName); + diagnostics.unresolved(targetName); + if (resolution.status() == NameResolver.Status.AMBIGUOUS) { + references.recordResolution(decl, targetName, resolution, ReferenceUsage.EXPRESSION); + } // An invalid basis remains explicitly unsupported in partial analysis. return new UnsupportedSymbol(new SymbolHeader(decl.id(), SymbolKind.UNSUPPORTED, DeclaredName.declared(decl.name()), decl.owner(), decl.order(), decl.origin(), decl.nameRange(), decl.language(), Documentation.empty(), MetaAttributeSet.empty(), Set.of(), List.of()), "VIEW_BASE", targetName.text()); } SymbolId target = resolution.target().orElseThrow(); - DeclInfo targetDecl = g.index.byId(target).orElseThrow(); - if (!building.contains(target)) g.buildSymbol(targetDecl); - else g.emitRule(decl, "cyclic view formation", "VIEW_CYCLE"); - g.recordReference(decl, targetName, target, ReferenceUsage.EXPRESSION); - var ownerSyntax = g.index.byId(decl.owner().orElseThrow()).orElseThrow().syntax(); + DeclInfo targetDecl = index.byId(target).orElseThrow(); + if (!building.contains(target)) ensureSymbol.apply(targetDecl); + else diagnostics.emitRule(decl, "cyclic view formation", "VIEW_CYCLE"); + references.recordReference(decl, targetName, target, ReferenceUsage.EXPRESSION); + var ownerSyntax = index.byId(decl.owner().orElseThrow()).orElseThrow().syntax(); boolean nullable = ownerSyntax instanceof ViewDeclarationSyntax syntax && syntax.formation().stream().flatMap(f -> f.bases().stream()) .anyMatch(b -> b.target().range().equals(targetName.range()) && b.orNull()); - return new ViewSymbol.Base(g.header(decl), target, nullable); + return new ViewSymbol.Base(symbolHeader.apply(decl), target, nullable); } ViewSymbol build(DeclInfo decl) { var syntax = (ViewDeclarationSyntax) decl.syntax(); if (!building.add(decl.id())) { - g.emitRule(decl, "cyclic view inheritance", "VIEW_CYCLE"); - return new ViewSymbol(g.header(decl), Optional.empty(), Optional.empty(), List.of(), List.of(), List.of(), List.of(), List.of()); + diagnostics.emitRule(decl, "cyclic view inheritance", "VIEW_CYCLE"); + return new ViewSymbol(symbolHeader.apply(decl), Optional.empty(), Optional.empty(), List.of(), List.of(), List.of(), List.of(), List.of()); } try { List bases = new ArrayList<>(); @@ -89,27 +139,34 @@ ViewSymbol build(DeclInfo decl) { Optional formation = Optional.empty(); if (syntax.baseView().isPresent() || syntax.modifiers().contains(Modifier.EXTENDED)) { var name = syntax.baseView().orElse(null); - parent = name == null ? g.baseTopicMember(decl.id(), decl.name(), Set.of(SymbolKind.VIEW)) - : g.resolver.resolve(decl.id(), name, Set.of(SymbolKind.VIEW), false).target(); - if (parent.isEmpty()) { if (name != null) g.unresolved(name); else g.emitRule(decl, "EXTENDED view needs an inherited view", "EXTENDED_BASE"); } + if (name == null) { + parent = inheritance.baseTopicMember(decl.id(), decl.name(), Set.of(SymbolKind.VIEW)); + } else { + var resolution = resolver.resolve(decl.id(), name, Set.of(SymbolKind.VIEW), false); + parent = resolution.target(); + if (resolution.status() == NameResolver.Status.AMBIGUOUS) { + references.recordResolution(decl, name, resolution, ReferenceUsage.EXPRESSION); + } + } + if (parent.isEmpty()) { if (name != null) diagnostics.unresolved(name); else diagnostics.emitRule(decl, "EXTENDED view needs an inherited view", "EXTENDED_BASE"); } else { - if (name != null) g.recordReference(decl, name, parent.orElseThrow(), ReferenceUsage.EXPRESSION); - var base = (ViewSymbol) g.buildSymbol(g.index.byId(parent.orElseThrow()).orElseThrow()); - g.baseOf.put(decl.id(), base.id()); - if (base.header().has(Modifier.FINAL)) g.emitRule(decl, "cannot extend FINAL view", "VIEW_FINAL"); + if (name != null) references.recordReference(decl, name, parent.orElseThrow(), ReferenceUsage.EXPRESSION); + var base = (ViewSymbol) ensureSymbol.apply(index.byId(parent.orElseThrow()).orElseThrow()); + inheritance.bindBase(decl.id(), base.id()); + if (base.header().has(Modifier.FINAL)) diagnostics.emitRule(decl, "cannot extend FINAL view", "VIEW_FINAL"); bases.addAll(base.bases()); result.addAll(base.members()); selections.addAll(base.selections()); extensionsByBase.addAll(base.baseExtensions()); formation = base.formation(); if (formation.orElse(null) instanceof ViewSymbol.Projection projected) projections.put(decl.id(), projected.base()); if (formation.orElse(null) instanceof ViewSymbol.Inspection inspected) inspections.put(decl.id(), inspected); } } members.put(decl.id(), result); - for (DeclInfo child : g.index.children(decl.id())) if (child.kind() == SymbolKind.VIEW_BASE) { - g.checkWork(); - var bound = g.buildSymbol(child); + for (DeclInfo child : index.children(decl.id())) if (child.kind() == SymbolKind.VIEW_BASE) { + work.checkpoint(); + var bound = ensureSymbol.apply(child); if (bound instanceof ViewSymbol.Base) { if (syntax.formation().stream().flatMap(f -> f.bases().stream()).anyMatch(b -> b.target() == child.syntax())) formationBases.add(child.id()); - if (bases.stream().anyMatch(id -> g.symbols.get(id).name().equals(bound.name()))) - g.emitRule(child, "duplicate view basis", "VIEW_ALIAS_CONFLICT"); + if (bases.stream().anyMatch(id -> state.symbol(id).name().equals(bound.name()))) + diagnostics.emitRule(child, "duplicate view basis", "VIEW_ALIAS_CONFLICT"); bases.add(child.id()); } } @@ -117,25 +174,25 @@ ViewSymbol build(DeclInfo decl) { List scope = new ArrayList<>(bases); scope.addAll(result); members.put(decl.id(), scope); for (var extension : syntax.baseExtensions()) { var baseId = member(decl.id(), extension.base().text()); - if (baseId.isEmpty() || !(g.symbols.get(baseId.orElseThrow()) instanceof ViewSymbol.Base original)) { - g.unresolved(extension.base()); continue; + if (baseId.isEmpty() || !(state.symbol(baseId.orElseThrow()) instanceof ViewSymbol.Base original)) { + diagnostics.unresolved(extension.base()); continue; } - g.recordReference(decl, extension.base(), original.id(), ReferenceUsage.EXPRESSION); + references.recordReference(decl, extension.base(), original.id(), ReferenceUsage.EXPRESSION); List extensions = new ArrayList<>(); for (var candidate : extension.extensions()) { var local = member(decl.id(), candidate.alias().text()); - if (local.isEmpty() || !(g.symbols.get(local.orElseThrow()) instanceof ViewSymbol.Base target)) continue; + if (local.isEmpty() || !(state.symbol(local.orElseThrow()) instanceof ViewSymbol.Base target)) continue; SymbolId current = target.target(); Set seen = new HashSet<>(); while (current != null && !current.equals(original.target()) && seen.add(current)) { - g.checkWork(); current = g.objectInheritanceBase(current).orElse(null); + work.checkpoint(); current = inheritance.objectInheritanceBase(current).orElse(null); } if (current == null || !current.equals(original.target())) - g.emitRule(decl, "view base extension must specialize its base", "VIEW_BASE_EXTENSION"); + diagnostics.emitRule(decl, "view base extension must specialize its base", "VIEW_BASE_EXTENSION"); extensions.add(target.id()); } extensionsByBase.add(new ViewSymbol.BaseExtension(original.id(), extensions)); } - if (parent.isPresent() && syntax.formation().isPresent()) g.emitRule(decl, "extended view cannot change formation", "VIEW_FORMATION_OVERRIDE"); + if (parent.isPresent() && syntax.formation().isPresent()) diagnostics.emitRule(decl, "extended view cannot change formation", "VIEW_FORMATION_OVERRIDE"); if (syntax.formation().isPresent()) { var f = syntax.formation().orElseThrow(); if (!formationBases.isEmpty()) { @@ -148,23 +205,23 @@ ViewSymbol build(DeclInfo decl) { case UNION -> formation = Optional.of(new ViewSymbol.Union(formationBases)); case AGGREGATION -> { List equal = new ArrayList<>(); - for (var path : f.paths()) equal.add(g.buildExpression(path, decl.id()).id()); + for (var path : f.paths()) equal.add(expressions.buildExpression(path, decl.id()).id()); formation = Optional.of(new ViewSymbol.Aggregation(formationBases.getFirst(), equal)); - var basis = (ViewSymbol.Base)g.symbols.get(formationBases.getFirst()); - TypeId element = g.nextTypeId(); - g.types.put(element, new StructureValueType(element, decl.origin(), Optional.of(basis.target()), List.of())); - TypeId bag = g.nextTypeId(); - g.types.put(bag, new CollectionType(bag, decl.origin(), CollectionKind.BAG, + var basis = (ViewSymbol.Base)state.symbol(formationBases.getFirst()); + TypeId element = typeBindings.nextTypeId(); + typeBindings.defineType(new StructureValueType(element, decl.origin(), Optional.of(basis.target()), List.of())); + TypeId bag = typeBindings.nextTypeId(); + typeBindings.defineType(new CollectionType(bag, decl.origin(), CollectionKind.BAG, new Cardinality(java.math.BigInteger.ZERO, new CardinalityUpper.Unbounded()), element)); SymbolId aggregateId = new SymbolId(decl.id().value() + ":AGGREGATES"); var h = new SymbolHeader(aggregateId, SymbolKind.ATTRIBUTE, DeclaredName.synthetic("AGGREGATES"), Optional.of(decl.id()), 0, new Origin.DerivedOrigin(basis.id(), DerivationKind.ALIAS), Optional.empty(), decl.language(), Documentation.empty(), MetaAttributeSet.empty(), Set.of(Modifier.TRANSIENT), List.of()); - g.symbols.put(aggregateId, new AttributeSymbol(h, bag, false, true, false, false, Optional.empty(), List.of())); - g.declaredTypes.put(aggregateId, bag); scope.add(aggregateId); + state.defineSymbol(new AttributeSymbol(h, bag, false, true, false, false, Optional.empty(), List.of())); + typeBindings.bindDeclaredType(aggregateId, bag); scope.add(aggregateId); } case INSPECTION, AREA_INSPECTION -> { - var basis = (ViewSymbol.Base)g.symbols.get(formationBases.getFirst()); + var basis = (ViewSymbol.Base)state.symbol(formationBases.getFirst()); var bound = bindInspection(decl, basis.target(), f.paths().getFirst(), f.kind() == ViewDeclarationSyntax.FormationKind.AREA_INSPECTION); if (bound != null) { @@ -174,87 +231,97 @@ ViewSymbol build(DeclInfo decl) { } } } - } else if (parent.isEmpty()) g.emitRule(decl, "view needs a formation or inherited view", "VIEW_FORMATION"); + } else if (parent.isEmpty()) diagnostics.emitRule(decl, "view needs a formation or inherited view", "VIEW_FORMATION"); for (var item : syntax.members()) { - g.checkWork(); + work.checkpoint(); if (item instanceof ViewDeclarationSyntax.AllOf all) { if (formation.orElse(null) instanceof ViewSymbol.Union || formation.orElse(null) instanceof ViewSymbol.Inspection inherited && inherited.area() || syntax.formation().map(f -> f.kind() == ViewDeclarationSyntax.FormationKind.AREA_INSPECTION).orElse(false)) - g.emitRule(decl, "ALL OF is not allowed in UNION or AREA INSPECTION", "VIEW_ALL_OF"); - var base = member(decl.id(), all.base().text()).map(g.symbols::get).orElse(null); - if (!(base instanceof ViewSymbol.Base basis)) { g.unresolved(all.base()); continue; } - g.recordReference(decl, all.base(), basis.nameRange().isPresent() ? basis.id() : basis.target(), ReferenceUsage.EXPRESSION); + diagnostics.emitRule(decl, "ALL OF is not allowed in UNION or AREA INSPECTION", "VIEW_ALL_OF"); + var base = member(decl.id(), all.base().text()).map(state::symbol).orElse(null); + if (!(base instanceof ViewSymbol.Base basis)) { diagnostics.unresolved(all.base()); continue; } + references.recordReference(decl, all.base(), basis.nameRange().isPresent() ? basis.id() : basis.target(), ReferenceUsage.EXPRESSION); List inherited; - if (g.symbols.get(basisTarget(basis)) instanceof ViewSymbol view) inherited = view.members(); + if (state.symbol(basisTarget(basis)) instanceof ViewSymbol view) inherited = view.members(); else { - g.index.byId(basisTarget(basis)).ifPresent(g::collectEffective); - inherited = g.effectiveMembers.getOrDefault(basisTarget(basis), List.of()).stream().map(EffectiveMember::declaredSymbol).toList(); + index.byId(basisTarget(basis)).ifPresent(inheritance::collectEffective); + inherited = state.effectiveMembers(basisTarget(basis)).stream().map(EffectiveMember::declaredSymbol).toList(); } for (SymbolId id : inherited) { - g.checkWork(); - if (g.symbols.get(id) instanceof RoleSymbol role && decl.language()==LanguageVersion.ILI24) { - includeRole(decl,basis,all,role,result,scope); + work.checkpoint(); + var original = state.symbol(id); + if (!(original instanceof AttributeSymbol) + && !(original instanceof RoleSymbol && decl.language() == LanguageVersion.ILI24)) continue; + var copyId = new SymbolId(decl.id().value() + ":all:" + basis.id().value() + ":" + id.value()); + if (state.hasSymbol(copyId)) { + diagnostics.emitRule(decl, "duplicate view attribute or basis name", "VIEW_ATTRIBUTE_DUPLICATE"); continue; } - if (!(g.symbols.get(id) instanceof AttributeSymbol attribute)) continue; - var copyId = new SymbolId(decl.id().value() + ":all:" + basis.id().value() + ":" + id.value()); - var h = new SymbolHeader(copyId, SymbolKind.ATTRIBUTE, attribute.name(), Optional.of(decl.id()), result.size(), - new Origin.DerivedOrigin(id, DerivationKind.ALIAS), Optional.empty(), decl.language(), - attribute.header().documentation(), attribute.header().metaAttributes(), attribute.header().modifiers(), List.of()); - var expressionId = new ExpressionId(copyId.value() + ":expression"); - g.expressions.put(expressionId, new PathExpression(expressionId, attribute.declaredType(), ExpressionTypeState.RESOLVED, - all.base().range(), List.of(), new ObjectPath(List.of(new PathStep.ViewBaseStep(basis.id(), basisTarget(basis), all.base().range()), - new PathStep.AttributeStep(attribute.name().value(), attribute.id(), all.base().range()))))); - var copy = new AttributeSymbol(h, attribute.declaredType(), attribute.mandatory() && !basis.orNull(), - attribute.transientAttribute(), attribute.continuous(), attribute.subdivision(), Optional.empty(), List.of(expressionId)); + AttributeSymbol copy; + if (original instanceof RoleSymbol role) { + copy = copyRole(decl, basis, all, role, copyId, result.size()); + } else { + var attribute = (AttributeSymbol) original; + var h = new SymbolHeader(copyId, SymbolKind.ATTRIBUTE, attribute.name(), Optional.of(decl.id()), result.size(), + new Origin.DerivedOrigin(id, DerivationKind.ALIAS), Optional.empty(), decl.language(), + attribute.header().documentation(), attribute.header().metaAttributes(), attribute.header().modifiers(), List.of()); + var expressionId = new ExpressionId(copyId.value() + ":expression"); + expressions.defineExpression(new PathExpression(expressionId, attribute.declaredType(), ExpressionTypeState.RESOLVED, + all.base().range(), List.of(), new ObjectPath(List.of(new PathStep.ViewBaseStep(basis.id(), basisTarget(basis), all.base().range()), + new PathStep.AttributeStep(attribute.name().value(), attribute.id(), all.base().range()))))); + copy = new AttributeSymbol(h, attribute.declaredType(), attribute.mandatory() && !basis.orNull(), + attribute.transientAttribute(), attribute.continuous(), attribute.subdivision(), Optional.empty(), List.of(expressionId)); + } + state.defineSymbol(copy); + typeBindings.bindDeclaredType(copy.id(), copy.declaredType()); add(decl, copy, result, scope); } } else { var attributeSyntax = ((ViewDeclarationSyntax.Attribute)item).declaration(); - var child = g.index.children(decl.id()).stream().filter(d -> d.syntax() == attributeSyntax).findFirst().orElseThrow(); - var attribute = (AttributeSymbol) g.buildSymbol(child); - var previous = result.stream().filter(id -> g.symbols.get(id).name().equals(attribute.name())).findFirst(); + var child = index.children(decl.id()).stream().filter(d -> d.syntax() == attributeSyntax).findFirst().orElseThrow(); + var attribute = (AttributeSymbol) ensureSymbol.apply(child); + var previous = result.stream().filter(id -> state.symbol(id).name().equals(attribute.name())).findFirst(); if (previous.isPresent() && attribute.header().has(Modifier.EXTENDED)) { - var old = (AttributeSymbol)g.symbols.get(previous.orElseThrow()); + var old = (AttributeSymbol)state.symbol(previous.orElseThrow()); if (old.owner().filter(decl.id()::equals).isPresent()) - g.emitRule(child, "EXTENDED requires an inherited attribute", "EXTENDED_BASE"); - g.validateAttributeOverrideProperties(child, attribute, old); - if (old.header().has(Modifier.FINAL)) g.emitRule(child, "cannot override FINAL attribute", "VIEW_FINAL"); - if (!g.assignable(attribute.declaredType(), old.declaredType())) - g.emitRule(child, "view override must specialize the inherited type", "VIEW_ATTRIBUTE_TYPE"); + diagnostics.emitRule(child, "EXTENDED requires an inherited attribute", "EXTENDED_BASE"); + inheritance.validateAttributeOverrideProperties(child, attribute, old); + if (old.header().has(Modifier.FINAL)) diagnostics.emitRule(child, "cannot override FINAL attribute", "VIEW_FINAL"); + if (!typeBindings.assignable(attribute.declaredType(), old.declaredType())) + diagnostics.emitRule(child, "view override must specialize the inherited type", "VIEW_ATTRIBUTE_TYPE"); result.set(result.indexOf(old.id()), attribute.id()); scope.remove(old.id()); scope.add(attribute.id()); - g.symbols.put(attribute.id(), new AttributeSymbol(attribute.header(), attribute.declaredType(), attribute.mandatory(), + state.replaceSymbol(new AttributeSymbol(attribute.header(), attribute.declaredType(), attribute.mandatory(), attribute.transientAttribute(), attribute.continuous(), attribute.subdivision(), Optional.of(old.id()), attribute.derivations())); } else { if (attribute.header().has(Modifier.EXTENDED)) - g.emitRule(child, "EXTENDED attribute needs an inherited member", "EXTENDED_BASE"); + diagnostics.emitRule(child, "EXTENDED attribute needs an inherited member", "EXTENDED_BASE"); add(decl, attribute, result, scope); } } } for (var selection : syntax.selections()) { - var expression = g.buildExpression(selection, decl.id()); selections.add(expression.id()); + var expression = expressions.buildExpression(selection, decl.id()); selections.add(expression.id()); if (!decl.modifiers().contains(Modifier.ABSTRACT)) validateConcreteExpression(decl, expression.id(), new HashSet<>()); - if (!expression.error() && !g.isBoolean(expression.typeId())) - g.emitTypeMismatch(decl.sourceId(), selection.range(), g.typeName(expression.typeId()), "BOOLEAN"); + if (!expression.error() && !typeBindings.isBoolean(expression.typeId())) + diagnostics.emitTypeMismatch(decl.sourceId(), selection.range(), typeBindings.typeName(expression.typeId()), "BOOLEAN"); } List constraints = new ArrayList<>(); - for (var child : g.index.children(decl.id())) if (child.kind() == SymbolKind.CONSTRAINT) { g.buildSymbol(child); constraints.add(child.id()); } + for (var child : index.children(decl.id())) if (child.kind() == SymbolKind.CONSTRAINT) { ensureSymbol.apply(child); constraints.add(child.id()); } if (syntax.endName().isPresent() && !syntax.name().equals(syntax.endName().orElseThrow())) - g.emitRule(decl, "view end name mismatch", "VIEW_END_NAME"); + diagnostics.emitRule(decl, "view end name mismatch", "VIEW_END_NAME"); List effective = new ArrayList<>(); for (var id : result) { - var attribute = (AttributeSymbol) g.symbols.get(id); + var attribute = (AttributeSymbol) state.symbol(id); if (!decl.modifiers().contains(Modifier.ABSTRACT) && attribute.header().has(Modifier.ABSTRACT)) - g.emitRule(decl, "concrete view contains abstract attribute", "VIEW_ABSTRACT_ATTRIBUTE"); + diagnostics.emitRule(decl, "concrete view contains abstract attribute", "VIEW_ABSTRACT_ATTRIBUTE"); if (attribute.mandatory()) { var used = new HashSet(); for (var expression : effectiveDerivations(attribute)) collectBases(expression, used, new HashSet<>()); - if (used.stream().anyMatch(base -> ((ViewSymbol.Base)g.symbols.get(base)).orNull())) - g.emitRule(decl, "mandatory view attribute uses an optional join basis", "VIEW_NULLABLE_ATTRIBUTE"); + if (used.stream().anyMatch(base -> ((ViewSymbol.Base)state.symbol(base)).orNull())) + diagnostics.emitRule(decl, "mandatory view attribute uses an optional join basis", "VIEW_NULLABLE_ATTRIBUTE"); if (formation.orElse(null) instanceof ViewSymbol.Union union && !used.containsAll(union.bases())) - g.emitRule(decl, "mandatory union attribute needs every basis", "VIEW_UNION_ATTRIBUTE"); + diagnostics.emitRule(decl, "mandatory union attribute needs every basis", "VIEW_UNION_ATTRIBUTE"); } if (!decl.modifiers().contains(Modifier.ABSTRACT)) { for (var expression : effectiveDerivations(attribute)) validateConcreteExpression(decl, expression, new HashSet<>()); @@ -262,7 +329,7 @@ ViewSymbol build(DeclInfo decl) { boolean local = attribute.owner().filter(decl.id()::equals).isPresent(); List path = new ArrayList<>(); if (!local && parent.isPresent()) { - g.effectiveMembers.getOrDefault(parent.orElseThrow(), List.of()).stream() + state.effectiveMembers(parent.orElseThrow()).stream() .filter(member -> member.declaredSymbol().equals(id)).findFirst() .ifPresent(member -> path.addAll(member.inheritancePath())); } @@ -271,8 +338,8 @@ ViewSymbol build(DeclInfo decl) { local ? (attribute.overrides().isPresent() ? EffectiveMemberOrigin.OVERRIDDEN : EffectiveMemberOrigin.LOCAL) : EffectiveMemberOrigin.INHERITED, attribute.overrides().isPresent())); } - g.effectiveMembers.put(decl.id(), List.copyOf(effective)); - return new ViewSymbol(g.header(decl), parent, formation, bases, extensionsByBase, selections, result, constraints); + inheritance.setEffectiveMembers(decl.id(), List.copyOf(effective)); + return new ViewSymbol(symbolHeader.apply(decl), parent, formation, bases, extensionsByBase, selections, result, constraints); } finally { building.remove(decl.id()); } } @@ -281,8 +348,8 @@ private List effectiveDerivations(AttributeSymbol attribute) { List chain = new ArrayList<>(); Set seen = new HashSet<>(); while (attribute != null && seen.add(attribute.id())) { - g.checkWork(); chain.add(attribute); - attribute = attribute.overrides().map(g.symbols::get).filter(AttributeSymbol.class::isInstance) + work.checkpoint(); chain.add(attribute); + attribute = attribute.overrides().map(state::symbol).filter(AttributeSymbol.class::isInstance) .map(AttributeSymbol.class::cast).orElse(null); } List result = new ArrayList<>(); @@ -294,151 +361,157 @@ private List effectiveDerivations(AttributeSymbol attribute) { * attributes. Their value is one participant of this basis row, irrespective * of the relationship cardinality. The source role stays reachable through * both origin and path; optional joins do not promise a mandatory value. */ - private void includeRole(DeclInfo owner,ViewSymbol.Base basis,ViewDeclarationSyntax.AllOf all, - RoleSymbol role,List result,List scope) { - var id=new SymbolId(owner.id().value()+":all:"+basis.id().value()+":"+role.id().value()); - var type=g.roleValueType(role,owner.id(),all.base().range()); + private AttributeSymbol copyRole(DeclInfo owner, ViewSymbol.Base basis, ViewDeclarationSyntax.AllOf all, + RoleSymbol role, SymbolId id, int position) { + var type=paths.roleValueType(role,owner.id(),all.base().range()); var modifiers=new HashSet<>(role.header().modifiers());modifiers.retainAll(Set.of(Modifier.ABSTRACT,Modifier.FINAL)); - var header=new SymbolHeader(id,SymbolKind.ATTRIBUTE,role.name(),Optional.of(owner.id()),result.size(), + var header=new SymbolHeader(id,SymbolKind.ATTRIBUTE,role.name(),Optional.of(owner.id()),position, new Origin.DerivedOrigin(role.id(),DerivationKind.ALIAS),Optional.empty(),owner.language(), role.header().documentation(),role.header().metaAttributes(),modifiers,List.of()); var expressionId=new ExpressionId(id.value()+":expression"); - g.expressions.put(expressionId,new PathExpression(expressionId,type,ExpressionTypeState.RESOLVED,all.base().range(),List.of(), + expressions.defineExpression(new PathExpression(expressionId,type,ExpressionTypeState.RESOLVED,all.base().range(),List.of(), new ObjectPath(List.of(new PathStep.ViewBaseStep(basis.id(),basisTarget(basis),all.base().range()), new PathStep.RoleStep(role.name().value(),role.id(),all.base().range()))))); - add(owner,new AttributeSymbol(header,type,false,false,false,false,Optional.empty(),List.of(expressionId)),result,scope); + return new AttributeSymbol(header,type,false,false,false,false,Optional.empty(),List.of(expressionId)); } record Inspected(ExpressionId path, SymbolId element, SymbolId parent, boolean area) {} /** The same decomposition binding is used by view formations and factors. */ Inspected bindInspection(DeclInfo decl, SymbolId source, PathExpressionSyntax path, boolean area) { - var expression = g.inspectionPath(path, decl.id(), source); + var expression = paths.inspectionPath(path, decl.id(), source); if (expression.error()) return null; - TypeDescriptor descriptor = g.effectiveDescriptor(expression.typeId()); + TypeDescriptor descriptor = typeBindings.effectiveDescriptor(expression.typeId()); SymbolId parent = source; if (path.segments().size() > 1) { var prefix = new PathExpressionSyntax(path.id(), path.range(), path.segments().subList(0, path.segments().size()-1)); - var result = g.inspectionPath(prefix, decl.id(), source); + var result = paths.inspectionPath(prefix, decl.id(), source); if (result.error()) return null; - var type = g.effectiveDescriptor(result.typeId()); - if (type instanceof CollectionType c) type = g.effectiveDescriptor(c.elementType()); - if (type instanceof StructureValueType st) parent = g.structures.target(st); + var type = typeBindings.effectiveDescriptor(result.typeId()); + if (type instanceof CollectionType c) type = typeBindings.effectiveDescriptor(c.elementType()); + if (type instanceof StructureValueType st) parent = structures.target(st); else if (type instanceof GeometryType geometry) parent = geometryElement(decl, geometry, false); } SymbolId element = null; - if (descriptor instanceof CollectionType collection) descriptor = g.effectiveDescriptor(collection.elementType()); - if (descriptor instanceof StructureValueType structure && !area) element = g.structures.target(structure); + if (descriptor instanceof CollectionType collection) descriptor = typeBindings.effectiveDescriptor(collection.elementType()); + if (descriptor instanceof StructureValueType structure && !area) element = structures.target(structure); else if (descriptor instanceof GeometryType geometry) { if (!area || geometry.geometryKind() == GeometryKind.AREA || geometry.geometryKind() == GeometryKind.MULTIAREA) element = geometryElement(decl, geometry, area); } if (element == null) { - g.emitRule(decl, "inspection requires a structure/geometry attribute, AREA requires an area", "VIEW_INSPECTION_TYPE"); + diagnostics.emitRule(decl, "inspection requires a structure/geometry attribute, AREA requires an area", "VIEW_INSPECTION_TYPE"); return null; } return new Inspected(expression.id(), element, parent, area); } - GraphBuilder.ExpressionResult factor(ExpressionId id, InspectionExpressionSyntax syntax, SymbolId owner) { - DeclInfo decl = g.index.byId(owner).orElseThrow(); + ExpressionBindings.ExpressionResult factor(ExpressionId id, InspectionExpressionSyntax syntax, SymbolId owner) { + DeclInfo decl = index.byId(owner).orElseThrow(); Optional view = Optional.empty(); SymbolId source = null; Inspected inspected = null; if (syntax.view().isPresent()) { var name = syntax.view().orElseThrow(); - var resolution = g.resolver.resolve(owner, name, Set.of(SymbolKind.VIEW), false); - if (resolution.target().isEmpty()) g.unresolved(name); - else { + var resolution = resolver.resolve(owner, name, Set.of(SymbolKind.VIEW), false); + if (resolution.target().isEmpty()) { + diagnostics.unresolved(name); + if (resolution.status() == NameResolver.Status.AMBIGUOUS) { + references.recordResolution(decl, name, resolution, ReferenceUsage.EXPRESSION); + } + } else { var target = resolution.target().orElseThrow(); - g.recordReference(decl, name, target, ReferenceUsage.EXPRESSION); - if (building.contains(target)) g.emitRule(decl, "cyclic inspection factor", "VIEW_CYCLE"); + references.recordReference(decl, name, target, ReferenceUsage.EXPRESSION); + if (building.contains(target)) diagnostics.emitRule(decl, "cyclic inspection factor", "VIEW_CYCLE"); else { - var bound = (ViewSymbol)g.buildSymbol(g.index.byId(target).orElseThrow()); + var bound = (ViewSymbol)ensureSymbol.apply(index.byId(target).orElseThrow()); if (bound.formation().orElse(null) instanceof ViewSymbol.Inspection i) { - view = Optional.of(target); source = ((ViewSymbol.Base)g.symbols.get(i.base())).target(); + view = Optional.of(target); source = ((ViewSymbol.Base)state.symbol(i.base())).target(); inspected = new Inspected(i.path(), i.element(), i.parent(), i.area()); } } } } else { var f = syntax.formation().orElseThrow(); var name = f.bases().getFirst().target(); - var resolution = g.resolver.resolve(owner, name, Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE, SymbolKind.VIEW, SymbolKind.ASSOCIATION), false); - if (resolution.target().isEmpty()) g.unresolved(name); - else { + var resolution = resolver.resolve(owner, name, Set.of(SymbolKind.CLASS, SymbolKind.STRUCTURE, SymbolKind.VIEW, SymbolKind.ASSOCIATION), false); + if (resolution.target().isEmpty()) { + diagnostics.unresolved(name); + if (resolution.status() == NameResolver.Status.AMBIGUOUS) { + references.recordResolution(decl, name, resolution, ReferenceUsage.EXPRESSION); + } + } else { source = resolution.target().orElseThrow(); - g.recordReference(decl, name, source, ReferenceUsage.EXPRESSION); - if (building.contains(source)) g.emitRule(decl, "cyclic inspection factor", "VIEW_CYCLE"); + references.recordReference(decl, name, source, ReferenceUsage.EXPRESSION); + if (building.contains(source)) diagnostics.emitRule(decl, "cyclic inspection factor", "VIEW_CYCLE"); else { - if (g.index.byId(source).orElseThrow().kind() == SymbolKind.VIEW) - g.buildSymbol(g.index.byId(source).orElseThrow()); + if (index.byId(source).orElseThrow().kind() == SymbolKind.VIEW) + ensureSymbol.apply(index.byId(source).orElseThrow()); inspected = bindInspection(decl, source, f.paths().getFirst(), f.kind() == ViewDeclarationSyntax.FormationKind.AREA_INSPECTION); } } } - var restriction = syntax.restriction().map(r -> g.buildExpression(r, owner)); + var restriction = syntax.restriction().map(r -> expressions.buildExpression(r, owner)); if (inspected == null) { - var diagnostic = g.emitRule(decl, "factor requires a bound inspection", "INSPECTION_FACTOR_TARGET"); - TypeId type = g.nextTypeId(); g.types.put(type, new ErrorType(type, new Origin.DeclaredOrigin(syntax.range()), List.of(diagnostic))); - g.expressions.put(id, new ErrorExpression(id, type, ExpressionTypeState.ERROR, syntax.range(), restriction.stream().map(GraphBuilder.ExpressionResult::id).toList(), List.of(diagnostic))); - return new GraphBuilder.ExpressionResult(id, type, true); + var diagnostic = diagnostics.emitRule(decl, "factor requires a bound inspection", "INSPECTION_FACTOR_TARGET"); + TypeId type = typeBindings.nextTypeId(); typeBindings.defineType(new ErrorType(type, new Origin.DeclaredOrigin(syntax.range()), List.of(diagnostic))); + expressions.defineExpression(new ErrorExpression(id, type, ExpressionTypeState.ERROR, syntax.range(), restriction.stream().map(ExpressionBindings.ExpressionResult::id).toList(), List.of(diagnostic))); + return new ExpressionBindings.ExpressionResult(id, type, true); } factorSources.computeIfAbsent(decl.id(), ignored -> new LinkedHashSet<>()).add(view.orElse(source)); - boolean error = restriction.map(GraphBuilder.ExpressionResult::error).orElse(false); + boolean error = restriction.map(ExpressionBindings.ExpressionResult::error).orElse(false); if (restriction.isPresent() && !error) { - TypeId expected = g.nextTypeId(); - g.types.put(expected, new ObjectArgumentType(expected, decl.origin(), Optional.of(source), List.of(), false)); - if (!g.assignable(restriction.orElseThrow().typeId(), expected)) { - g.emitRule(decl, "inspection restriction requires one object specializing its source", "INSPECTION_FACTOR_RESTRICTION"); + TypeId expected = typeBindings.nextTypeId(); + typeBindings.defineType(new ObjectArgumentType(expected, decl.origin(), Optional.of(source), List.of(), false)); + if (!typeBindings.assignable(restriction.orElseThrow().typeId(), expected)) { + diagnostics.emitRule(decl, "inspection restriction requires one object specializing its source", "INSPECTION_FACTOR_RESTRICTION"); error = true; } } - TypeId element = g.nextTypeId(); g.types.put(element, new StructureValueType(element, decl.origin(), Optional.of(inspected.element()), List.of())); - TypeId bag = g.nextTypeId(); g.types.put(bag, new CollectionType(bag, decl.origin(), CollectionKind.BAG, Cardinality.unboundedFrom(0), element)); - g.expressions.put(id, new InspectionExpression(id, bag, error ? ExpressionTypeState.ERROR : ExpressionTypeState.RESOLVED, - syntax.range(), view, source, inspected.element(), inspected.path(), restriction.map(GraphBuilder.ExpressionResult::id), inspected.area())); - return new GraphBuilder.ExpressionResult(id, bag, error); + TypeId element = typeBindings.nextTypeId(); typeBindings.defineType(new StructureValueType(element, decl.origin(), Optional.of(inspected.element()), List.of())); + TypeId bag = typeBindings.nextTypeId(); typeBindings.defineType(new CollectionType(bag, decl.origin(), CollectionKind.BAG, Cardinality.unboundedFrom(0), element)); + expressions.defineExpression(new InspectionExpression(id, bag, error ? ExpressionTypeState.ERROR : ExpressionTypeState.RESOLVED, + syntax.range(), view, source, inspected.element(), inspected.path(), restriction.map(ExpressionBindings.ExpressionResult::id), inspected.area())); + return new ExpressionBindings.ExpressionResult(id, bag, error); } private void requireDependency(DeclInfo decl, SymbolId source) { - SymbolId topic = g.scopeRoot(decl.id()), target = g.scopeRoot(source); - if (g.index.byId(topic).map(d -> d.kind() == SymbolKind.TOPIC).orElse(false) - && g.index.byId(target).map(d -> d.kind() == SymbolKind.TOPIC).orElse(false) - && !g.isTopicSubtype(topic, target) && !g.topicDependencies(topic).contains(target)) - g.emitRule(decl, "inspection source topic requires DEPENDS ON", "VIEW_TOPIC_DEPENDENCY"); + SymbolId topic = paths.scopeRoot(decl.id()), target = paths.scopeRoot(source); + if (index.byId(topic).map(d -> d.kind() == SymbolKind.TOPIC).orElse(false) + && index.byId(target).map(d -> d.kind() == SymbolKind.TOPIC).orElse(false) + && !topics.isTopicSubtype(topic, target) && !topics.topicDependencies(topic).contains(target)) + diagnostics.emitRule(decl, "inspection source topic requires DEPENDS ON", "VIEW_TOPIC_DEPENDENCY"); } private void add(DeclInfo owner, AttributeSymbol attribute, List result, List scope) { - if (scope.stream().anyMatch(id -> g.symbols.get(id).name().equals(attribute.name()))) - g.emitRule(owner, "duplicate view attribute or basis name", "VIEW_ATTRIBUTE_DUPLICATE"); - g.symbols.put(attribute.id(), attribute); g.declaredTypes.put(attribute.id(), attribute.declaredType()); + if (scope.stream().anyMatch(id -> state.symbol(id).name().equals(attribute.name()))) + diagnostics.emitRule(owner, "duplicate view attribute or basis name", "VIEW_ATTRIBUTE_DUPLICATE"); result.add(attribute.id()); scope.add(attribute.id()); } SymbolId geometryElement(DeclInfo owner, GeometryType geometry, boolean area) { TypeId vertex = geometry.vertexDomain().map(id -> { - g.index.byId(id).ifPresent(g::buildSymbol); return g.declaredTypes.get(id); + index.byId(id).ifPresent(ensureSymbol::apply); return state.declaredType(id); }).orElse(null); if (vertex == null) { - g.emitUnsupported(owner.sourceId(), owner.syntax().range(), "INSPECTION_GEOMETRY_VERTEX", "geometry inspection needs a bound vertex type"); + diagnostics.emitUnsupported(owner.sourceId(), owner.syntax().range(), "INSPECTION_GEOMETRY_VERTEX", "geometry inspection needs a bound vertex type"); return syntheticStructure(owner, geometry, "LineSegment", Map.of()); } var context = segmentContexts.computeIfAbsent(owner.id().value() + ":" + geometry.id().value(), - key -> new SegmentContextualizer(g, this, owner, geometry)); - var segment = context.symbol(g.lines.predefined(owner, "INTERLIS.LineSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow()); - var start = context.symbol(g.lines.predefined(owner, "INTERLIS.StartSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow()); + key -> new SegmentContextualizer(state, work, index, this, owner, geometry, inheritance, typeBindings, expressions, lineForms, ensureSymbol)); + var segment = context.symbol(lineForms.predefined(owner, "INTERLIS.LineSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow()); + var start = context.symbol(lineForms.predefined(owner, "INTERLIS.StartSegment", Set.of(SymbolKind.STRUCTURE)).orElseThrow()); TypeId segments = collection(owner, segment); var lineGeometry = syntheticStructure(owner, geometry, "LineGeometry", Map.of("Segments", segments)); - g.lines.startConstraint(owner, lineGeometry, segment, start, owner.syntax().range()); + lineForms.startConstraint(owner, lineGeometry, segment, start, owner.syntax().range()); registerGeometryElement(lineGeometry, geometry, context, GeometryElementRole.LINE_GEOMETRY); if (geometry.geometryKind() == GeometryKind.POLYLINE || geometry.geometryKind() == GeometryKind.MULTIPOLYLINE) return lineGeometry; - TypeId polyline = g.nextTypeId(); - g.types.put(polyline, new GeometryType(polyline, geometry.origin(), GeometryKind.POLYLINE, geometry.lineForms(), geometry.vertexDomain(), geometry.lineAttributes(), true, geometry.withoutOverlaps(), geometry.overlap())); + TypeId polyline = typeBindings.nextTypeId(); + typeBindings.defineType(new GeometryType(polyline, geometry.origin(), GeometryKind.POLYLINE, geometry.lineForms(), geometry.vertexDomain(), geometry.lineAttributes(), true, geometry.withoutOverlaps(), geometry.overlap())); Map fields = new LinkedHashMap<>(); fields.put("Geometry", polyline); if (owner.language() == LanguageVersion.ILI23) { - TypeId lineAttrs = g.nextTypeId(); + TypeId lineAttrs = typeBindings.nextTypeId(); if (geometry.lineAttributes().isPresent()) { - SymbolId target = geometry.lineAttributes().orElseThrow(); g.index.byId(target).ifPresent(g::buildSymbol); - g.types.put(lineAttrs, new StructureValueType(lineAttrs, geometry.origin(), Optional.of(target), List.of())); - } else g.types.put(lineAttrs, new GenericType(lineAttrs, geometry.origin(), "ANYSTRUCTURE")); + SymbolId target = geometry.lineAttributes().orElseThrow(); index.byId(target).ifPresent(ensureSymbol::apply); + typeBindings.defineType(new StructureValueType(lineAttrs, geometry.origin(), Optional.of(target), List.of())); + } else typeBindings.defineType(new GenericType(lineAttrs, geometry.origin(), "ANYSTRUCTURE")); fields.put("LineAttrs", lineAttrs); } var edge = syntheticStructure(owner, geometry, "SurfaceEdge", fields); @@ -450,36 +523,36 @@ SymbolId geometryElement(DeclInfo owner, GeometryType geometry, boolean area) { } private TypeId collection(DeclInfo owner, SymbolId structure) { - TypeId element = g.nextTypeId(); g.types.put(element, new StructureValueType(element, owner.origin(), Optional.of(structure), List.of())); - TypeId list = g.nextTypeId(); g.types.put(list, new CollectionType(list, owner.origin(), CollectionKind.LIST, + TypeId element = typeBindings.nextTypeId(); typeBindings.defineType(new StructureValueType(element, owner.origin(), Optional.of(structure), List.of())); + TypeId list = typeBindings.nextTypeId(); typeBindings.defineType(new CollectionType(list, owner.origin(), CollectionKind.LIST, Cardinality.unboundedFrom(0), element)); return list; } private SymbolId syntheticStructure(DeclInfo owner, GeometryType geometry, String name, Map fields) { SymbolId id = new SymbolId(owner.id().value() + ":inspection:" + geometry.id().value() + ":" + name); - if (g.symbols.containsKey(id)) return id; - var canonical = g.lines.predefined(owner, "INTERLIS." + name, Set.of(SymbolKind.STRUCTURE)).orElseThrow(); + if (state.hasSymbol(id)) return id; + var canonical = lineForms.predefined(owner, "INTERLIS." + name, Set.of(SymbolKind.STRUCTURE)).orElseThrow(); var header = new SymbolHeader(id, SymbolKind.STRUCTURE, DeclaredName.synthetic(name), Optional.of(owner.id()), 0, new Origin.DerivedOrigin(canonical, DerivationKind.CONTEXTUALIZATION), Optional.empty(), owner.language(), Documentation.empty(), MetaAttributeSet.empty(), Set.of(), List.of()); List attributes = new ArrayList<>(); List effective = new ArrayList<>(); for (var field : fields.entrySet()) { SymbolId attr = new SymbolId(id.value() + ":" + field.getKey()); - var template = (StructureSymbol)g.symbols.get(canonical); - var original = template.members().stream().filter(member -> g.symbols.get(member).name().value().equals(field.getKey())).findFirst().orElseThrow(); + var template = (StructureSymbol)state.symbol(canonical); + var original = template.members().stream().filter(member -> state.symbol(member).name().value().equals(field.getKey())).findFirst().orElseThrow(); var h = new SymbolHeader(attr, SymbolKind.ATTRIBUTE, DeclaredName.synthetic(field.getKey()), Optional.of(id), attributes.size(), new Origin.DerivedOrigin(original, DerivationKind.CONTEXTUALIZATION), Optional.empty(), owner.language(), Documentation.empty(), MetaAttributeSet.empty(), Set.of(), List.of()); - g.symbols.put(attr, new AttributeSymbol(h, field.getValue(), field.getKey().equals("SegmentEndPoint") || field.getKey().equals("ArcPoint"), false, false, false, Optional.empty(), List.of())); - g.declaredTypes.put(attr, field.getValue()); attributes.add(attr); + state.defineSymbol(new AttributeSymbol(h, field.getValue(), field.getKey().equals("SegmentEndPoint") || field.getKey().equals("ArcPoint"), false, false, false, Optional.empty(), List.of())); + typeBindings.bindDeclaredType(attr, field.getValue()); attributes.add(attr); effective.add(new EffectiveMember(attr, id, List.of(id), effective.size(), EffectiveMemberOrigin.LOCAL, false)); } - members.put(id, attributes); g.effectiveMembers.put(id, effective); - g.symbols.put(id, new StructureSymbol(header, Optional.empty(), attributes, List.of())); return id; + members.put(id, attributes); inheritance.setEffectiveMembers(id, effective); + state.defineSymbol(new StructureSymbol(header, Optional.empty(), attributes, List.of())); return id; } private void collectBases(ExpressionId id, Set bases, Set seen) { if (!seen.add(id)) return; - g.checkWork(); var expression = g.expressions.get(id); + work.checkpoint(); var expression = state.expression(id); if (expression instanceof PathExpression path) for (var step : path.path().steps()) if (step instanceof PathStep.ViewBaseStep base) bases.add(base.base()); if(expression instanceof FunctionCallExpression call)call.argumentPaths().values().forEach(p->p.binding().ifPresent(binding->collectBases(binding,bases,seen))); @@ -488,10 +561,10 @@ private void collectBases(ExpressionId id, Set bases, Set seen) { if (!seen.add(id)) return; - g.checkWork(); var expression = g.expressions.get(id); + work.checkpoint(); var expression = state.expression(id); if (expression instanceof PathExpression path) for (var step : path.path().steps()) { - if (step instanceof PathStep.AttributeStep attribute && g.symbols.get(attribute.target()).header().has(Modifier.ABSTRACT)) - g.emitRule(owner, "concrete view refers to an abstract attribute", "VIEW_ABSTRACT_ATTRIBUTE"); + if (step instanceof PathStep.AttributeStep attribute && state.symbol(attribute.target()).header().has(Modifier.ABSTRACT)) + diagnostics.emitRule(owner, "concrete view refers to an abstract attribute", "VIEW_ABSTRACT_ATTRIBUTE"); } if(expression instanceof FunctionCallExpression call)call.argumentPaths().values().forEach(p->p.binding().ifPresent(binding->validateConcreteExpression(owner,binding,seen))); expression.children().forEach(child -> validateConcreteExpression(owner, child, seen)); @@ -499,20 +572,20 @@ private void validateConcreteExpression(DeclInfo owner, ExpressionId id, Set d.code().value().equals("ILI-SEM-0002"))); + var ambiguousPath = ambiguous.analysis().semanticGraph().references().stream() + .filter(ref -> ref.resolution() instanceof BindingResolution.UnresolvedTarget unresolved + && unresolved.reason() == UnresolvedReason.AMBIGUOUS_NAME).findFirst().orElseThrow(); + var resolution = assertInstanceOf(BindingResolution.UnresolvedTarget.class, ambiguousPath.resolution()); + assertEquals(2, resolution.candidates().size()); + assertTrue(ambiguousPath.segments().stream().allMatch(segment -> segment.target().isEmpty())); rejects("2.4", base.replace("owner --", "owner (EXTERNAL) --")); succeeds("2.4", base.replace("a --", "a (EXTERNAL) --")); rejects("2.4", base.replace("a -- {1} A", "a -- {1} Wrong.A")); @@ -182,6 +189,20 @@ private static CompilationOutcome.Rejected rejects(String version, String body) """); } + @Test void aMissingRoleTargetDoesNotBecomeAnInventedDeclaration() { + var rejected = rejects("2.4", model("1", "single(a)") + .replace("a -- {1} A", "a -- {1} MissingClassWithLongName")); + var analysis = rejected.analysis(); + var reference = analysis.semanticGraph().references().stream() + .filter(ref -> ref.usage() == ReferenceUsage.ROLE_TARGET + && ref.segments().getFirst().text().equals("MissingClassWithLongName")) + .findFirst().orElseThrow(); + var range = reference.range(); + var cursor = new guru.interlis.source.SourceRange(range.source(), range.revision(), + range.startOffset(), range.startOffset()); + assertTrue(LanguageQueries.create().symbolAt(analysis, cursor, CancellationToken.none()).known().isEmpty()); + } + @Test void inheritsAccessThroughClassIdentityAndHiding() { succeeds("2.4", model("1", "single(a)").replace("CONSTRAINTS OF Owner", "CLASS Child EXTENDS Owner = END Child; CONSTRAINTS OF Child")); diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/CompilerEngineTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/CompilerEngineTest.java index 815c52ae..579bfdac 100644 --- a/interlis-compiler/src/test/java/guru/interlis/compiler/CompilerEngineTest.java +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/CompilerEngineTest.java @@ -1,10 +1,13 @@ package guru.interlis.compiler; import guru.interlis.diagnostics.Diagnostic; +import guru.interlis.diagnostics.Phase; import guru.interlis.execution.CancellationToken; +import guru.interlis.model.AnalysisCoverage; import guru.interlis.model.ClassSymbol; import guru.interlis.model.CompiledSchema; import guru.interlis.model.ConstraintSymbol; +import guru.interlis.model.CoverageStatus; import guru.interlis.model.EffectiveMember; import guru.interlis.model.EffectiveType; import guru.interlis.model.MandatoryConstraint; @@ -22,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -75,6 +79,13 @@ void validImportedDomainAndConstraintCompiles() { CompilationOutcome outcome = CompilerEngines.create().compile(request, CancellationToken.none()); CompilationOutcome.Succeeded succeeded = assertInstanceOf( CompilationOutcome.Succeeded.class, outcome, () -> describe(outcome)); + var phases = List.of(Phase.LEXING, Phase.PARSING, Phase.DECLARATION_INDEXING, Phase.BINDING, + Phase.TYPE_CHECKING, Phase.SEMANTIC_CHECKING); + var expectedCoverage = List.of(survey.id(), base.id()).stream().flatMap(source -> + phases.stream().map(phase -> new AnalysisCoverage.CoverageEntry(phase, Optional.of(source), + CoverageStatus.COMPLETE, Optional.empty(), Optional.empty()))).toList(); + assertEquals(expectedCoverage, succeeded.analysis().coverage().entries()); + CompiledSchema schema = succeeded.schema(); assertEquals(1, schema.rootModels().size()); assertEquals(2, schema.models().size()); @@ -122,6 +133,75 @@ void unresolvedDomainRejectsClosure() { assertTrue(rejected.rejection().reasons().contains(RejectionReason.SOURCE_ERRORS)); assertTrue(rejected.analysis().diagnostics().stream() .anyMatch(d -> d.code().value().equals("ILI-SEM-0001"))); + var coverage = rejected.analysis().coverage(); + assertEquals(CoverageStatus.BLOCKED_BY_ERROR, coverage.entry(Phase.BINDING, Optional.of(survey.id())).status()); + assertEquals(CoverageStatus.COMPLETE, coverage.entry(Phase.BINDING, Optional.of(base.id())).status()); + } + + @Test + void boundNonBooleanConstraintBlocksTypeChecking() { + SourceText source = CompilerTestSupport.source("memory:/constraint.ili", """ + INTERLIS 2.4; + MODEL F AT "x" VERSION "1" = + TOPIC T = CLASS C = + value : TEXT; + MANDATORY CONSTRAINT value; + END C; END T; + END F. + """); + var request = CompilerTestSupport.request(List.of("F"), Map.of(source.id(), source), + List.of(), CompilerOptions.defaults()); + var rejected = assertInstanceOf(CompilationOutcome.Rejected.class, + CompilerEngines.create().compile(request, CancellationToken.none())); + var analysis = rejected.analysis(); + assertTrue(analysis.requestDiagnostics().stream().anyMatch(d -> d.code().value().equals("ILI-SEM-0005"))); + assertEquals(CoverageStatus.COMPLETE, analysis.coverage().entry(Phase.BINDING, Optional.of(source.id())).status()); + assertEquals(CoverageStatus.BLOCKED_BY_ERROR, + analysis.coverage().entry(Phase.TYPE_CHECKING, Optional.of(source.id())).status()); + } + + @Test + void rejectedTypeDescriptorsBlockTypeCheckingEvenWithGenericRuleDiagnostics() { + SourceText source = CompilerTestSupport.source("memory:/invalid-type.ili", + "INTERLIS 2.4; MODEL F AT \"x\" VERSION \"1\" = DOMAIN Empty = TEXT*0; END F."); + var request = CompilerTestSupport.request(List.of("F"), Map.of(source.id(), source), + List.of(), CompilerOptions.defaults()); + var rejected = assertInstanceOf(CompilationOutcome.Rejected.class, + CompilerEngines.create().compile(request, CancellationToken.none())); + var analysis = rejected.analysis(); + assertTrue(analysis.requestDiagnostics().stream().anyMatch(d -> d.phase() == Phase.SEMANTIC_CHECKING + && d.arguments().toString().contains("TEXT_LENGTH"))); + assertEquals(CoverageStatus.BLOCKED_BY_ERROR, + analysis.coverage().entry(Phase.TYPE_CHECKING, Optional.of(source.id())).status()); + assertEquals(CoverageStatus.COMPLETE, + analysis.coverage().entry(Phase.BINDING, Optional.of(source.id())).status()); + } + + @Test + void unsupportedExpressionHasAFeatureEntryWithTheOriginalSourceAndDiagnostic() { + SourceText source = CompilerTestSupport.source("memory:/unsupported-expression.ili", """ + INTERLIS 2.4; + MODEL F AT "x" VERSION "1" = + TOPIC T = CLASS C = + value : TEXT; + MANDATORY CONSTRAINT ALL OF value; + END C; END T; + END F. + """); + var request = CompilerTestSupport.request(List.of("F"), Map.of(source.id(), source), + List.of(), CompilerOptions.defaults()); + var rejected = assertInstanceOf(CompilationOutcome.Rejected.class, + CompilerEngines.create().compile(request, CancellationToken.none())); + assertEquals(Set.of(RejectionReason.INCOMPLETE_SUPPORT), rejected.rejection().reasons()); + var analysis = rejected.analysis(); + assertEquals(CoverageStatus.NOT_IMPLEMENTED, + analysis.coverage().entry(Phase.SEMANTIC_CHECKING, Optional.of(source.id())).status()); + var entry = analysis.coverage().entries().stream() + .filter(e -> e.featureId().equals(Optional.of("ALL_OF_PATH"))).findFirst().orElseThrow(); + assertEquals(Optional.of(source.id()), entry.source()); + assertEquals(CoverageStatus.NOT_IMPLEMENTED, entry.status()); + assertTrue(analysis.requestDiagnostics().stream().anyMatch(d -> Optional.of(d.id()).equals(entry.diagnosticId()) + && d.code().value().equals("ILI-IMPL-0001"))); } @Test diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/EditorPartialStateContractTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/EditorPartialStateContractTest.java index 6f41b235..6b199ba1 100644 --- a/interlis-compiler/src/test/java/guru/interlis/compiler/EditorPartialStateContractTest.java +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/EditorPartialStateContractTest.java @@ -1,6 +1,7 @@ package guru.interlis.compiler; import guru.interlis.compiler.query.*; +import guru.interlis.diagnostics.Phase; import guru.interlis.execution.CancellationToken; import guru.interlis.model.*; import java.util.*; @@ -14,6 +15,8 @@ class EditorPartialStateContractTest { String text="INTERLIS "+version+";MODEL F AT \"x\" VERSION \"1\"=TOPIC T=CLASS Broken=missing: ;END Broken;CLASS Neighbor=value:TEXT*4;END Neighbor;CLASS Use EXTENDS Neighbor=END Use;END T;END F."; var source=CompilerTestSupport.source("memory:/editor-partial.ili",text);var request=CompilerTestSupport.request(List.of("F"),Map.of(source.id(),source),List.of(),CompilerOptions.defaults()); var engine=CompilerEngines.create();var analysis=engine.analyze(request,CancellationToken.none());assertFalse(analysis.diagnostics().isEmpty()); + assertEquals(CoverageStatus.BLOCKED_BY_ERROR, + analysis.coverage().entry(Phase.PARSING, Optional.of(source.id())).status()); var neighbor=analysis.queries().findQualifiedName(QualifiedName.of("F","T","Neighbor")).getFirst();assertInstanceOf(ClassSymbol.class,analysis.queries().symbol(neighbor).known().orElseThrow()); var value=analysis.queries().findQualifiedName(QualifiedName.of("F","T","Neighbor","value")).getFirst(); assertEquals(4,assertInstanceOf(TextType.class,analysis.queries().declaredType(value).known().orElseThrow()).maxLength().orElseThrow()); @@ -24,4 +27,22 @@ class EditorPartialStateContractTest { var rejected=assertInstanceOf(CompilationOutcome.Rejected.class,engine.compile(request,CancellationToken.none()));assertEquals(Set.of(RejectionReason.SOURCE_ERRORS),rejected.rejection().reasons()); } } + + @Test void versionRestrictedSyntaxBlocksParsingCoverageInTheEditor() { + for (String version : List.of("2.3", "2.4")) { + var source = CompilerTestSupport.source("memory:/date.ili", + "INTERLIS " + version + "; MODEL F AT \"x\" VERSION \"1\" = " + + "TOPIC T = CLASS C = value : DATE; END C; END T; END F."); + var request = CompilerTestSupport.request(List.of("F"), Map.of(source.id(), source), + List.of(), CompilerOptions.defaults()); + var analysis = CompilerEngines.create().analyze(request, CancellationToken.none()); + boolean restricted = version.equals("2.3"); + assertEquals(restricted, analysis.requestDiagnostics().stream() + .anyMatch(d -> d.code().value().equals("ILI-SYN-0006") && d.phase() == Phase.PARSING)); + assertEquals(CoverageStatus.COMPLETE, + analysis.coverage().entry(Phase.LEXING, Optional.of(source.id())).status()); + assertEquals(restricted ? CoverageStatus.BLOCKED_BY_ERROR : CoverageStatus.COMPLETE, + analysis.coverage().entry(Phase.PARSING, Optional.of(source.id())).status()); + } + } } diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/GraphicBindingContractTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/GraphicBindingContractTest.java index 4b3124ff..cefd1319 100644 --- a/interlis-compiler/src/test/java/guru/interlis/compiler/GraphicBindingContractTest.java +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/GraphicBindingContractTest.java @@ -126,6 +126,14 @@ static List bindings(Map COMPILER_PHASES = + List.of(Phase.SOURCE_RESOLUTION, Phase.LEXING, Phase.DECLARATION_INDEXING, Phase.BINDING); + private static CompilationRequest input(String body,CompilerOptions options) { var source=new SourceText(new SourceId("memory:/boundary.ili"),new SourceRevision(9),"INTERLIS 2.4;MODEL F AT \"https://example.org\" VERSION \"1\"="+body+"END F."); return CompilerTestSupport.request(List.of("F"),Map.of(source.id(),source),List.of(),options); } private static CompilationRequest input(String body){return input(body,CompilerOptions.defaults());} - private static CompilerEngine engine(RunObserver observer) {return CompilerEngines.create(new CompilerServices(observer,ClockSource.system(),RunContextFactory.defaults()));} + private static CompilerEngine engine(RunObserver observer) { + ClockSource clock = new ClockSource() { + private long ticks; + + @Override public long wallTimeEpochMillis() { return 0; } + @Override public long monotonicNanos() { return ++ticks; } + }; + return CompilerEngines.create(new CompilerServices(observer, clock, RunContextFactory.defaults())); + } + @Test void cancellationAtEveryEmittedCompilerPhasePreservesInputsAndAllowsAFollowup() { var baselineEvents=new ArrayList();var request=input("DOMAIN D=TEXT;"); assertInstanceOf(CompilationOutcome.Succeeded.class,engine(baselineEvents::add).compile(request,CancellationToken.none())); - var phases=baselineEvents.stream().filter(RunEvent.PhaseStarted.class::isInstance).map(RunEvent.PhaseStarted.class::cast).map(RunEvent.PhaseStarted::phase).distinct().toList(); - assertEquals(Set.of(Phase.SOURCE_RESOLUTION,Phase.LEXING,Phase.DECLARATION_INDEXING,Phase.BINDING),Set.copyOf(phases)); - for(var phase:phases) { - var events=new ArrayList();var cancelled=new AtomicBoolean();var armed=new AtomicBoolean(true); - var engine=engine(event->{events.add(event);if(event instanceof RunEvent.PhaseStarted started&&started.phase()==phase&&armed.compareAndSet(true,false))cancelled.set(true);}); - var source=request.sources().sources().values().iterator().next();String text=source.text(); - var outcome=assertInstanceOf(CompilationOutcome.Cancelled.class,engine.compile(request,cancelled::get));assertEquals(CancellationReason.USER_REQUESTED,outcome.reason()); - assertEquals(1,events.stream().filter(RunEvent.Finished.class::isInstance).count());assertEquals(text,source.text());assertEquals(new SourceRevision(9),source.revision()); - cancelled.set(false);events.clear();var next=assertInstanceOf(CompilationOutcome.Succeeded.class,engine.compile(request,cancelled::get)); - var fresh=assertInstanceOf(CompilationOutcome.Succeeded.class,CompilerEngines.create().compile(request,CancellationToken.none())); - assertEquals(fresh.analysis().diagnostics(),next.analysis().diagnostics());assertEquals(fresh.schema().fingerprint(),next.schema().fingerprint()); - assertEquals(1,events.stream().filter(RunEvent.Finished.class::isInstance).count()); + assertInstanceOf(RunCompletion.Succeeded.class, assertFinishedRun(baselineEvents, Phase.BINDING, PhaseStatus.SUCCEEDED)); + for (var phase : COMPILER_PHASES) { + var events = new ArrayList(); + var remaining = new AtomicInteger(-1); + var armed = new AtomicBoolean(true); + var engine = engine(event -> { + events.add(event); + if (event instanceof RunEvent.PhaseStarted started && started.phase() == phase + && armed.compareAndSet(true, false)) { + // Let binding reach its semantic work checkpoints before requesting cancellation. + remaining.set(phase == Phase.BINDING ? 3 : 1); + } + }); + CancellationToken token = () -> remaining.get() >= 0 + && remaining.updateAndGet(count -> Math.max(0, count - 1)) == 0; + var source = request.sources().sources().values().iterator().next(); + String text = source.text(); + var outcome = assertInstanceOf(CompilationOutcome.Cancelled.class, engine.compile(request, token)); + assertEquals(CancellationReason.USER_REQUESTED, outcome.reason()); + var completion = assertInstanceOf(RunCompletion.Cancelled.class, + assertFinishedRun(events, phase, PhaseStatus.CANCELLED)); + assertEquals(outcome.reason(), completion.reason()); + assertEquals(text, source.text()); + assertEquals(new SourceRevision(9), source.revision()); + + remaining.set(-1); + events.clear(); + var next = assertInstanceOf(CompilationOutcome.Succeeded.class, engine.compile(request, token)); + var fresh = assertInstanceOf(CompilationOutcome.Succeeded.class, + CompilerEngines.create().compile(request, CancellationToken.none())); + assertEquals(fresh.analysis().diagnostics(), next.analysis().diagnostics()); + assertEquals(fresh.schema().fingerprint(), next.schema().fingerprint()); + assertInstanceOf(RunCompletion.Succeeded.class, + assertFinishedRun(events, Phase.BINDING, PhaseStatus.SUCCEEDED)); } } + + @Test void sourceRejectionKeepsTechnicallyCompletedPhasesSuccessful() { + var events = new ArrayList(); + var outcome = assertInstanceOf(CompilationOutcome.Rejected.class, engine(events::add) + .compile(input("TOPIC T=CLASS C=x:Missing;END C;END T;"), CancellationToken.none())); + assertEquals(Set.of(RejectionReason.SOURCE_ERRORS), outcome.rejection().reasons()); + var completion = assertInstanceOf(RunCompletion.Rejected.class, + assertFinishedRun(events, Phase.BINDING, PhaseStatus.SUCCEEDED)); + assertEquals(Set.of("SOURCE_ERRORS"), completion.reasons()); + } + @Test void repeatedMalformedSourcesAndDeeplyNestedValidSourcesTerminateWithExactOutcomeKinds() { var engine=CompilerEngines.create(); for(String malformed:List.of("TOPIC T=CLASS C=x:;END C;END T;","/* outer /* inner */","DOMAIN D=((((;"))for(int i=0;i<20;i++) { @@ -56,6 +101,64 @@ private static CompilationRequest input(String body,CompilerOptions options) { assertInstanceOf(CompilationOutcome.Succeeded.class,engine.compile(valid,CancellationToken.none())); assertFalse(events.toString().contains(secret));assertFalse(events.toString().contains("STRUCTURE S")); } + + @Test void technicalFaultsInBindingCloseTheActivePhaseAsFailed() { + for (boolean fatal : List.of(false, true)) { + var events = new ArrayList(); + var inBinding = new AtomicBoolean(); + var polls = new AtomicInteger(); + var engine = engine(event -> { + events.add(event); + if (event instanceof RunEvent.PhaseStarted started && started.phase() == Phase.BINDING) { + inBinding.set(true); + } + }); + String secret = "private test service detail"; + CancellationToken faultingToken = () -> { + if (inBinding.get() && polls.incrementAndGet() == 3) { + if (fatal) throw new AssertionError(secret); + throw new IllegalStateException(secret); + } + return false; + }; + var factory = RunContextFactory.defaults(); + var context = factory.createChild(factory.createRoot()); + var request = input("DOMAIN D=TEXT;"); + if (fatal) { + var error = assertThrows(AssertionError.class, + () -> engine.compile(request, AnalysisReuse.none(), context, faultingToken)); + assertEquals(secret, error.getMessage()); + assertPhaseEvents(events, Phase.BINDING, PhaseStatus.FAILED); + assertFalse(events.stream().anyMatch(e -> + e instanceof RunEvent.Finished end && end.completion() instanceof RunCompletion.Succeeded)); + } else { + var failed = assertInstanceOf(CompilationOutcome.Failed.class, + engine.compile(request, AnalysisReuse.none(), context, faultingToken)); + var completion = assertInstanceOf(RunCompletion.Failed.class, + assertFinishedRun(events, Phase.BINDING, PhaseStatus.FAILED)); + assertEquals(failed.failureId(), completion.failureId()); + assertFalse(failed.toString().contains(secret)); + assertFalse(events.toString().contains(secret)); + } + assertEquals(3, polls.get()); + assertEquals(context.runId(), events.getFirst().header().runId()); + assertEquals(context.parentRunId(), events.getFirst().header().parentRunId()); + } + } + + @Test void semanticWorkBudgetCancelsTheActiveBindingPhase() { + var events = new ArrayList(); + var defaults = CompilerOptions.defaults(); + var options = new CompilerOptions(defaults.semanticProfile(), defaults.allowedLanguages(), defaults.warningPolicy(), + defaults.budget().withMaxSemanticWorkUnits(1), defaults.overlays()); + var outcome = assertInstanceOf(CompilationOutcome.Cancelled.class, engine(events::add) + .compile(input("DOMAIN D=TEXT;", options), CancellationToken.none())); + assertEquals(CancellationReason.RESOURCE_LIMIT, outcome.reason()); + var completion = assertInstanceOf(RunCompletion.Cancelled.class, + assertFinishedRun(events, Phase.BINDING, PhaseStatus.CANCELLED)); + assertEquals(outcome.reason(), completion.reason()); + } + @Test void syntaxNodeRecoveryAndTokenBudgetsRemainResourceCancellationRatherThanLanguageErrors() { var b=AnalysisBudget.defaults(); for(String resource:List.of("maxSyntaxNodes","maxRecoveryEvents","maxTokens")) { @@ -63,8 +166,12 @@ private static CompilationRequest input(String body,CompilerOptions options) { resource.equals("maxSyntaxNodes")?1:b.maxSyntaxNodes(),resource.equals("maxRecoveryEvents")?1:b.maxRecoveryEvents(),b.maxParseRuleDepth(),b.maxSemanticWorkUnits(),b.maxDiagnostics(),b.maxNumericLiteralDigits(),b.maxAbsoluteDecimalExponent()); var defaults=CompilerOptions.defaults();var options=new CompilerOptions(defaults.semanticProfile(),defaults.allowedLanguages(),defaults.warningPolicy(),limited,defaults.overlays()); String body=resource.equals("maxRecoveryEvents")?"STRUCTURE S=a:;b:;c:;END S;":"DOMAIN D=TEXT;"; - var request=input(body,options);var outcome=assertInstanceOf(CompilationOutcome.Cancelled.class,CompilerEngines.create().compile(request,CancellationToken.none()),resource); + var events = new ArrayList(); + var request=input(body,options);var outcome=assertInstanceOf(CompilationOutcome.Cancelled.class,engine(events::add).compile(request,CancellationToken.none()),resource); assertEquals(CancellationReason.RESOURCE_LIMIT,outcome.reason()); + var completion = assertInstanceOf(RunCompletion.Cancelled.class, + assertFinishedRun(events, Phase.LEXING, PhaseStatus.CANCELLED)); + assertEquals(outcome.reason(), completion.reason()); if(!resource.equals("maxTokens")) { var source=request.sources().sources().values().iterator().next(); var syntax=guru.interlis.syntax.SyntaxServices.create().parse(source,new guru.interlis.syntax.ParseOptions(guru.interlis.syntax.ParseMode.EDITOR,Optional.empty(),limited),CancellationToken.none()); @@ -76,4 +183,34 @@ private static CompilationRequest input(String body,CompilerOptions options) { var fragment=guru.interlis.syntax.SyntaxServices.create().parseFragment(source,source.range(0,source.length()),guru.interlis.syntax.FragmentKind.EXPRESSION,guru.interlis.model.LanguageVersion.ILI24,CancellationToken.none()); assertFalse(fragment.consumedEntireFragment());assertTrue(fragment.diagnostics().stream().anyMatch(d->d.code().value().equals("ILI-INF-0002"))); } + + private static RunCompletion assertFinishedRun(List events, Phase lastPhase, PhaseStatus status) { + assertPhaseEvents(events, lastPhase, status); + assertEquals(1, events.stream().filter(RunEvent.Finished.class::isInstance).count()); + return assertInstanceOf(RunEvent.Finished.class, events.getLast()).completion(); + } + + private static void assertPhaseEvents(List events, Phase lastPhase, PhaseStatus status) { + var start = assertInstanceOf(RunEvent.Started.class, events.getFirst()).header(); + assertEquals(1, events.stream().filter(RunEvent.Started.class::isInstance).count()); + for (int i = 0; i < events.size(); i++) { + var header = events.get(i).header(); + assertEquals(i + 1L, header.sequence()); + assertEquals(start.runId(), header.runId()); + assertEquals(start.parentRunId(), header.parentRunId()); + } + + int count = COMPILER_PHASES.indexOf(lastPhase) + 1; + var phases = events.stream() + .filter(e -> e instanceof RunEvent.PhaseStarted || e instanceof RunEvent.PhaseFinished).toList(); + assertEquals(2 * count, phases.size()); + for (int i = 0; i < count; i++) { + var opened = assertInstanceOf(RunEvent.PhaseStarted.class, phases.get(2 * i)); + var closed = assertInstanceOf(RunEvent.PhaseFinished.class, phases.get(2 * i + 1)); + assertEquals(COMPILER_PHASES.get(i), opened.phase()); + assertEquals(opened.phase(), closed.phase()); + assertEquals(opened.metadata().unitCount(), closed.unitCount()); + assertEquals(i == count - 1 ? status : PhaseStatus.SUCCEEDED, closed.status()); + } + } } diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/PredefinedBindingContractTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/PredefinedBindingContractTest.java index f5df6133..93d92c23 100644 --- a/interlis-compiler/src/test/java/guru/interlis/compiler/PredefinedBindingContractTest.java +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/PredefinedBindingContractTest.java @@ -1,8 +1,10 @@ package guru.interlis.compiler; import guru.interlis.compiler.query.LanguageQueries; +import guru.interlis.diagnostics.DiagnosticArgument; import guru.interlis.execution.CancellationToken; import guru.interlis.model.*; +import guru.interlis.model.query.QueryResult; import guru.interlis.model.query.TypeRelations; import guru.interlis.source.*; import java.util.*; @@ -140,27 +142,116 @@ else assertEquals(name.equals("HALIGNMENT") ? AlignmentKind.HORIZONTAL : Alignme } @Test void ambiguousImportedFunctionsKeepTheirBindingCauseAndQualifiedControls() { - for (var version : List.of("2.3", "2.4")) for (boolean qualified : List.of(false, true)) { + for (var version : List.of("2.3", "2.4")) for (String name : List.of("f", "Base.f", "missing", "Wrong.f")) { var base = source("memory:/base.ili", model(version, "FUNCTION f(v:NUMERIC):BOOLEAN;") .replace("MODEL F", "MODEL Base").replace("END F.", "END Base.")); var other = source("memory:/other.ili", base.text().replace("MODEL Base", "MODEL Other").replace("END Base.", "END Other.")); var use = source("memory:/use.ili", model(version, "IMPORTS UNQUALIFIED Base, UNQUALIFIED Other; " - + "TOPIC T=CLASS C=MANDATORY CONSTRAINT " + (qualified ? "Base.f" : "f") + "(1); END C; END T;")); + + "TOPIC T=CLASS C=MANDATORY CONSTRAINT " + name + "(1); END C; END T;")); var request = request(List.of("F"), Map.of(base.id(), base, other.id(), other, use.id(), use), List.of(binding(parse(use), "Base", base, parse(base)), binding(parse(use), "Other", other, parse(other))), CompilerOptions.defaults()); var result = CompilerEngines.create().compile(request, TOKEN); - if (qualified) assertInstanceOf(CompilationOutcome.Succeeded.class, result); - else { - var rejected = assertInstanceOf(CompilationOutcome.Rejected.class, result); - assertEquals(Set.of(RejectionReason.SOURCE_ERRORS), rejected.rejection().reasons()); - assertEquals(List.of("ILI-SEM-0002"), rejected.analysis().diagnostics().stream().map(d -> d.code().value()).toList()); - var cause = rejected.analysis().diagnostics().getFirst().id(); - assertTrue(rejected.analysis().semanticGraph().types().stream().filter(ErrorType.class::isInstance) - .map(ErrorType.class::cast).anyMatch(type -> type.diagnosticIds().contains(cause))); + int start = use.text().indexOf(name + "(1)"); + var range = use.range(start, start + name.length()); + var cursor = use.range(range.endOffset() - 1, range.endOffset() - 1); + var queries = LanguageQueries.create(); + if (name.equals("Base.f")) { + var analysis = assertInstanceOf(CompilationOutcome.Succeeded.class, result).analysis(); + var target = analysis.queries().findQualifiedName(QualifiedName.of("Base", "f")).getFirst(); + var reference = expressionReferenceAt(analysis, range); + assertEquals(new BindingResolution.BoundTarget(target.symbol()), reference.resolution()); + assertEquals(Optional.of(target.symbol()), reference.segments().getLast().target()); + assertEquals(target, queries.definition(analysis, cursor, TOKEN).known().orElseThrow().symbol()); + continue; } + var rejected = assertInstanceOf(CompilationOutcome.Rejected.class, result); + var analysis = rejected.analysis(); + boolean ambiguous = name.equals("f"); + var reason = ambiguous ? UnresolvedReason.AMBIGUOUS_NAME : UnresolvedReason.NAME_NOT_FOUND; + var candidates = ambiguous ? List.of("Base", "Other").stream() + .map(m -> analysis.queries().findQualifiedName(QualifiedName.of(m, "f")).getFirst().symbol()) + .sorted(Comparator.comparing(SymbolId::value)).toList() : List.of(); + var reference = expressionReferenceAt(analysis, range); + var unresolved = assertInstanceOf(BindingResolution.UnresolvedTarget.class, reference.resolution()); + assertEquals(reason, unresolved.reason()); + assertEquals(candidates, unresolved.candidates()); + assertEquals(List.of(name.split("\\.")), reference.segments().stream().map(s -> use.slice(s.range())).toList()); + assertTrue(reference.segments().stream().allMatch(segment -> segment.target().isEmpty())); + if (ambiguous) assertThrows(UnsupportedOperationException.class, () -> unresolved.candidates().clear()); + + assertEquals(Set.of(RejectionReason.SOURCE_ERRORS), rejected.rejection().reasons()); + assertEquals(List.of(ambiguous ? "ILI-SEM-0002" : "ILI-SEM-0001"), + analysis.diagnostics().stream().map(d -> d.code().value()).toList()); + var diagnostic = analysis.diagnostics().getFirst(); + assertEquals(Optional.of(range), diagnostic.primaryRange()); + assertEquals(new DiagnosticArgument.IdentifierArgument("name", name), diagnostic.arguments().getFirst()); + if (ambiguous) assertEquals(new DiagnosticArgument.IdentifierListArgument("candidates", + candidates.stream().map(SymbolId::value).toList()), diagnostic.arguments().getLast()); + assertTrue(analysis.semanticGraph().types().stream().filter(ErrorType.class::isInstance) + .map(ErrorType.class::cast).anyMatch(type -> type.diagnosticIds().contains(diagnostic.id()))); + + var candidateRefs = candidates.stream().map(id -> new SymbolRef(analysis.snapshotId(), id)).toList(); + assertEquals(new QueryResult.Unresolved<>(reason, candidateRefs), queries.symbolAt(analysis, cursor, TOKEN)); + assertTrue(queries.definition(analysis, cursor, TOKEN).known().isEmpty()); + assertTrue(queries.hover(analysis, cursor, TOKEN).known().isEmpty()); + assertFalse(queries.prepareRename(analysis, cursor, TOKEN).allowed()); + for (var candidate : candidateRefs) { + assertFalse(analysis.queries().referencesTo(candidate).contains(reference)); + } + } + } + + private static NameReference expressionReferenceAt(AnalysisSnapshot analysis, SourceRange range) { + var references = analysis.semanticGraph().references().stream() + .filter(r -> r.usage() == ReferenceUsage.EXPRESSION && r.range().equals(range)).toList(); + assertEquals(1, references.size()); + return references.getFirst(); + } + + @Test void importedTopicBasesRespectTheImportVisibility() { + for (var version : List.of("2.3", "2.4")) for (var baseName : List.of("Shared", "Missing", "Wrong.Shared", "Base.Shared")) { + assertTopicBase(version, baseName, true); + } + for (var version : List.of("2.3", "2.4")) { + assertTopicBase(version, "Shared", false); + assertTopicBase(version, "Base.Shared", false); } } + private static void assertTopicBase(String version, String name, boolean unqualified) { + var base = source("memory:/base.ili", model(version, "TOPIC Shared=END Shared;") + .replace("MODEL F", "MODEL Base").replace("END F.", "END Base.")); + var other = source("memory:/other.ili", base.text() + .replace("MODEL Base", "MODEL Other").replace("END Base.", "END Other.")); + var imports = unqualified ? "IMPORTS UNQUALIFIED Base, UNQUALIFIED Other;" : "IMPORTS Base, Other;"; + var use = source("memory:/use.ili", model(version, imports + + "TOPIC Derived EXTENDS " + name + "=END Derived;")); + var request = request(List.of("F"), Map.of(base.id(), base, other.id(), other, use.id(), use), + List.of(binding(parse(use), "Base", base, parse(base)), binding(parse(use), "Other", other, parse(other))), + CompilerOptions.defaults()); + var result = CompilerEngines.create().compile(request, TOKEN); + if (name.equals("Base.Shared")) { + var success = assertInstanceOf(CompilationOutcome.Succeeded.class, result); + var ref = success.analysis().semanticGraph().references().stream() + .filter(r -> r.usage() == ReferenceUsage.BASE_TYPE).findFirst().orElseThrow(); + var baseTopic = success.analysis().queries().findQualifiedName(QualifiedName.of("Base", "Shared")).getFirst(); + assertEquals(new BindingResolution.BoundTarget(baseTopic.symbol()), ref.resolution()); + return; + } + var rejected = assertInstanceOf(CompilationOutcome.Rejected.class, result); + assertEquals(Set.of(RejectionReason.SOURCE_ERRORS), rejected.rejection().reasons()); + var reference = rejected.analysis().semanticGraph().references().stream() + .filter(ref -> ref.usage() == ReferenceUsage.BASE_TYPE).findFirst().orElseThrow(); + var resolution = assertInstanceOf(BindingResolution.UnresolvedTarget.class, reference.resolution()); + boolean ambiguous = unqualified && name.equals("Shared"); + assertEquals(ambiguous ? UnresolvedReason.AMBIGUOUS_NAME : UnresolvedReason.NAME_NOT_FOUND, resolution.reason()); + assertEquals(ambiguous ? 2 : 0, resolution.candidates().size()); + assertEquals(name, use.slice(reference.range())); + String code = ambiguous ? "ILI-SEM-0002" : "ILI-SEM-0001"; + assertTrue(rejected.analysis().diagnostics().stream().anyMatch(d -> d.code().value().equals(code) + && d.primaryRange().equals(Optional.of(reference.range())))); + } + @Test void validInvalidValidEditsKeepFreshAndReusedModelsEquivalent() { var engine = CompilerEngines.create(); for (var version : List.of("2.3", "2.4")) { diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/RequestDiagnosticsContractTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/RequestDiagnosticsContractTest.java index 9ead3623..bc589f06 100644 --- a/interlis-compiler/src/test/java/guru/interlis/compiler/RequestDiagnosticsContractTest.java +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/RequestDiagnosticsContractTest.java @@ -2,6 +2,7 @@ import guru.interlis.diagnostics.*; import guru.interlis.execution.CancellationToken; +import guru.interlis.model.CoverageStatus; import guru.interlis.source.*; import java.util.*; import org.junit.jupiter.api.Test; @@ -39,10 +40,18 @@ static AnalysisSnapshot analysis(CompilationOutcome out) { var out = CompilerEngines.create().compile(input("Good", sources, WarningPolicy.ALLOW_WARNINGS), TOKEN); var a = assertInstanceOf(CompilationOutcome.Succeeded.class, out).analysis(); assertFalse(a.diagnostics().isEmpty()); assertTrue(a.requestDiagnostics().isEmpty()); + assertEquals(CoverageStatus.COMPLETE, + a.coverage().entry(Phase.BINDING, Optional.of(sources.getFirst().id())).status()); + if (sources.size() == 2) { + assertEquals(CoverageStatus.NOT_REQUESTED, + a.coverage().entry(Phase.BINDING, Optional.of(sources.getLast().id())).status()); + } assertThrows(UnsupportedOperationException.class, () -> a.requestDiagnostics().clear()); var bad = assertInstanceOf(CompilationOutcome.Rejected.class, CompilerEngines.create() .compile(input("Bad", sources, WarningPolicy.ALLOW_WARNINGS), TOKEN)); assertFalse(bad.analysis().requestDiagnostics().isEmpty()); + assertEquals(CoverageStatus.BLOCKED_BY_ERROR, + bad.analysis().coverage().entry(Phase.BINDING, Optional.of(sources.getLast().id())).status()); assertEquals(bad.analysis().requestDiagnostics().stream().map(Diagnostic::id).sorted(Comparator.comparing(DiagnosticId::value)).toList(), bad.rejection().diagnosticIds()); for (var diagnostic : bad.analysis().requestDiagnostics()) assertSame(diagnostic, bad.analysis().diagnostics().stream().filter(d -> d.id().equals(diagnostic.id())).findFirst().orElseThrow()); @@ -59,8 +68,10 @@ static AnalysisSnapshot analysis(CompilationOutcome out) { .compile(input("Other", List.of(shared), WarningPolicy.REJECT_WARNINGS), TOKEN)); assertTrue(relevant.rejection().reasons().contains(RejectionReason.WARNINGS_REJECTED)); assertTrue(relevant.analysis().requestDiagnostics().stream().anyMatch(d -> d.severity() == Severity.WARNING)); - assertInstanceOf(CompilationOutcome.Succeeded.class, CompilerEngines.create() + var allowed = assertInstanceOf(CompilationOutcome.Succeeded.class, CompilerEngines.create() .compile(input("Other", List.of(shared), WarningPolicy.ALLOW_WARNINGS), TOKEN)); + assertEquals(CoverageStatus.COMPLETE, + allowed.analysis().coverage().entry(Phase.PARSING, Optional.of(shared.id())).status()); } @Test void requiredFileSyntaxErrorsStayVisibleEvenOutsideTheSelectedModelRange() { @@ -73,6 +84,8 @@ static AnalysisSnapshot analysis(CompilationOutcome out) { var rejected = assertInstanceOf(CompilationOutcome.Rejected.class, CompilerEngines.create() .compile(input("Good", List.of(together), WarningPolicy.ALLOW_WARNINGS), TOKEN)); assertTrue(rejected.analysis().requestDiagnostics().stream().anyMatch(d -> d.phase() == Phase.PARSING)); + assertEquals(CoverageStatus.BLOCKED_BY_ERROR, + rejected.analysis().coverage().entry(Phase.PARSING, Optional.of(together.id())).status()); } @Test void importsAndMissingOrAmbiguousRootsRemainVisible() { @@ -97,7 +110,29 @@ static AnalysisSnapshot analysis(CompilationOutcome out) { var request = new CompilationRequest(input.roots(), SourceSet.of(input.sources().sources(), List.of(), List.of(global), ResolutionFingerprint.none()), input.options()); var rejected = assertInstanceOf(CompilationOutcome.Rejected.class, CompilerEngines.create().compile(request, TOKEN)); assertTrue(rejected.analysis().requestDiagnostics().stream().anyMatch(d -> d.code().equals(global.code()) && d.arguments().equals(global.arguments()) && d.primaryRange().isEmpty())); - assertTrue(rejected.rejection().reasons().contains(RejectionReason.ENVIRONMENT_FAILURE)); + assertEquals(Set.of(RejectionReason.ENVIRONMENT_FAILURE), rejected.rejection().reasons()); + assertTrue(rejected.analysis().coverage().incompleteEntries().isEmpty()); + } + + @Test void unlocatedOverlayBindingErrorKeepsItsGlobalDiagnosticLink() { + var source = source("good", model("Good", "DOMAIN D = TEXT;")); + var input = input("Good", List.of(source), WarningPolicy.ALLOW_WARNINGS); + var overlay = new SemanticOverlays.SemanticOverlay.MetaAttributeOverlay( + new SemanticOverlays.OverlaySource("config:/project", 1), + SemanticOverlays.Target.named("Good.Missing"), Map.of("label", List.of("test"))); + var defaults = input.options(); + var options = new CompilerOptions(defaults.semanticProfile(), defaults.allowedLanguages(), + defaults.warningPolicy(), defaults.budget(), new SemanticOverlays(List.of(overlay))); + var request = new CompilationRequest(input.roots(), input.sources(), options); + var rejected = assertInstanceOf(CompilationOutcome.Rejected.class, CompilerEngines.create().compile(request, TOKEN)); + var analysis = rejected.analysis(); + var diagnostic = analysis.requestDiagnostics().stream().filter(d -> d.code().value().equals("ILI-SEM-0011")) + .findFirst().orElseThrow(); + assertTrue(diagnostic.primaryRange().isEmpty()); + var entry = analysis.coverage().entry(Phase.BINDING, Optional.empty()); + assertEquals(CoverageStatus.BLOCKED_BY_ERROR, entry.status()); + assertEquals(Optional.of(diagnostic.id()), entry.diagnosticId()); + assertEquals(CoverageStatus.COMPLETE, analysis.coverage().entry(Phase.BINDING, Optional.of(source.id())).status()); } @Test void rootAndRevisionSwitchesWithReuseMatchFreshAndLeavePublishedSelectionsUntouched() { @@ -142,12 +177,15 @@ static AnalysisSnapshot analysis(CompilationOutcome out) { var out = assertInstanceOf(CompilationOutcome.Rejected.class, CompilerEngines.create().compile(request, TOKEN)); assertTrue(out.analysis().requestDiagnostics().stream().anyMatch(d -> d.code().value().equals("ILI-SYN-0004"))); assertFalse(out.rejection().diagnosticIds().isEmpty()); + assertSyntaxOnlyCoverage(out.analysis(), damaged.id()); } // An unassociated file cannot acquire a model identity from its basename. var named = input("Lost", List.of(damaged), WarningPolicy.ALLOW_WARNINGS); var out = assertInstanceOf(CompilationOutcome.Rejected.class, CompilerEngines.create().compile(named, TOKEN)); assertTrue(out.analysis().requestDiagnostics().stream().anyMatch(d -> d.code().value().equals("ILI-RES-0001"))); assertFalse(out.analysis().requestDiagnostics().stream().anyMatch(d -> d.code().value().equals("ILI-SYN-0004"))); + assertEquals(CoverageStatus.NOT_REQUESTED, + out.analysis().coverage().entry(Phase.PARSING, Optional.of(damaged.id())).status()); } @Test void aResolvedImportKeepsItsRequiredSyntaxSourceEvenWhenThePromisedModelIsLost() { @@ -160,12 +198,21 @@ static AnalysisSnapshot analysis(CompilationOutcome out) { assertTrue(out.analysis().requestDiagnostics().stream().anyMatch(d -> d.code().value().equals("ILI-SYN-0004") && d.primaryRange().orElseThrow().source().equals(damaged.id()))); assertTrue(out.analysis().requestDiagnostics().stream().anyMatch(d -> d.code().value().equals("ILI-RES-0004"))); + assertSyntaxOnlyCoverage(out.analysis(), damaged.id()); var unassociated = new CompilationRequest(request.roots(), SourceSet.of(request.sources().sources()), request.options()); var missing = assertInstanceOf(CompilationOutcome.Rejected.class, CompilerEngines.create().compile(unassociated, TOKEN)); assertTrue(missing.analysis().requestDiagnostics().stream().anyMatch(d -> d.code().value().equals("ILI-RES-0001"))); assertFalse(missing.analysis().requestDiagnostics().stream().anyMatch(d -> d.code().value().equals("ILI-SYN-0004"))); } + private static void assertSyntaxOnlyCoverage(AnalysisSnapshot analysis, SourceId source) { + var coverage = analysis.coverage(); + var entries = coverage.entries().stream().filter(entry -> entry.source().equals(Optional.of(source))).toList(); + assertEquals(List.of(Phase.LEXING, Phase.PARSING), entries.stream().map(entry -> entry.phase()).toList()); + assertEquals(CoverageStatus.COMPLETE, coverage.entry(Phase.LEXING, Optional.of(source)).status()); + assertEquals(CoverageStatus.BLOCKED_BY_ERROR, coverage.entry(Phase.PARSING, Optional.of(source)).status()); + } + @Test void namespaceConflictsCarryTheActualRevisionBoundDeclaration() { for (int shift : List.of(0, 2)) { var source = new SourceText(new SourceId("memory:/🦊.ili"), new SourceRevision(shift + 7), @@ -173,6 +220,8 @@ static AnalysisSnapshot analysis(CompilationOutcome out) { var a = analysis(CompilerEngines.create().compile(input("F", List.of(source), WarningPolicy.ALLOW_WARNINGS), TOKEN)); var d = a.requestDiagnostics().stream().filter(x -> x.code().value().equals("ILI-SEM-0010")).findFirst().orElseThrow(); assertTrue(d.arguments().toString().contains("SCOPE_OUTER_TYPE_NAME")); + assertEquals(CoverageStatus.BLOCKED_BY_ERROR, + a.coverage().entry(Phase.SEMANTIC_CHECKING, Optional.of(source.id())).status()); var related = d.relatedLocations().getFirst().range(); assertEquals(source.id(), related.source()); assertEquals(source.revision(), related.revision()); assertEquals(source.text().indexOf("Office"), related.startOffset()); assertEquals("Office", source.text().substring(related.startOffset(), related.endOffset())); assertTrue(d.primaryRange().orElseThrow().startOffset() > related.startOffset()); diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/ScopeNamespaceContractTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/ScopeNamespaceContractTest.java index 0d0e1c52..54f063bc 100644 --- a/interlis-compiler/src/test/java/guru/interlis/compiler/ScopeNamespaceContractTest.java +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/ScopeNamespaceContractTest.java @@ -1,6 +1,10 @@ package guru.interlis.compiler; import guru.interlis.execution.CancellationToken; +import guru.interlis.model.BindingResolution; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.SymbolId; +import guru.interlis.model.UnresolvedReason; import java.util.*; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -26,6 +30,21 @@ private static CompilationRequest request(String version,String body) { @Test void typeAndRuntimeParameterNamesRemainDifferentNamespaces() { for(String version:List.of("2.3","2.4"))assertInstanceOf(CompilationOutcome.Succeeded.class,CompilerEngines.create().compile(request(version,"DOMAIN Value=TEXT;PARAMETER Value:TEXT;TOPIC T=CLASS C=Value:TEXT;END C;END T;"),CancellationToken.none())); } + @Test void ambiguousBaseKeepsOneReferenceAfterTheFinalReferenceWalk() { + for (String version : List.of("2.3", "2.4")) { + var result = assertInstanceOf(CompilationOutcome.Rejected.class, CompilerEngines.create().compile( + request(version, "TOPIC T=CLASS A=END A;CLASS A=END A;CLASS B EXTENDS A=END B;END T;"), + CancellationToken.none())); + var graph = result.analysis().semanticGraph(); + var uses = graph.references().stream().filter(ref -> ref.usage() == ReferenceUsage.BASE_TYPE).toList(); + assertEquals(1, uses.size()); + var resolution = assertInstanceOf(BindingResolution.UnresolvedTarget.class, uses.getFirst().resolution()); + assertEquals(UnresolvedReason.AMBIGUOUS_NAME, resolution.reason()); + assertEquals(graph.symbols().stream().filter(symbol -> symbol.name().value().equals("A")) + .map(symbol -> symbol.id()).sorted(Comparator.comparing(SymbolId::value)).toList(), + resolution.candidates()); + } + } @Test void anonymousAssociationsDoNotInventCollidingUserTypeNames() { for(String version:List.of("2.3","2.4")) { var out=CompilerEngines.create().compile(request(version, diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/SemanticBuildIsolationTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/SemanticBuildIsolationTest.java new file mode 100644 index 00000000..8729a0e3 --- /dev/null +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/SemanticBuildIsolationTest.java @@ -0,0 +1,86 @@ +package guru.interlis.compiler; + +import guru.interlis.compiler.SemanticOverlays.OverlaySource; +import guru.interlis.compiler.SemanticOverlays.SemanticOverlay.MetaAttributeOverlay; +import guru.interlis.compiler.SemanticOverlays.Target; +import guru.interlis.diagnostics.Phase; +import guru.interlis.execution.CancellationToken; +import guru.interlis.execution.RunEvent; +import guru.interlis.model.DomainSymbol; +import guru.interlis.model.NumericType; +import guru.interlis.source.SourceId; +import guru.interlis.source.SourceRevision; +import guru.interlis.source.SourceText; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +class SemanticBuildIsolationTest { + @Test + void overlappingCompilesUseTheirOwnSourcesAndOverlaysAndKeepEarlierGraph() throws Exception { + var parallel = new AtomicBoolean(); + var timedOut = new AtomicBoolean(); + var binding = new CountDownLatch(2); + var defaults = CompilerServices.defaults(); + var engine = CompilerEngines.create(new CompilerServices(event -> { + if (parallel.get() && event instanceof RunEvent.PhaseStarted start && start.phase() == Phase.BINDING) { + binding.countDown(); + try { + if (!binding.await(10, TimeUnit.SECONDS)) timedOut.set(true); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + timedOut.set(true); + } + } + }, defaults.clock(), defaults.runContexts())); + var old = compile(engine, input("memory:/old.ili", 1, "old", 10)); + parallel.set(true); + + CompilationOutcome.Succeeded first; + CompilationOutcome.Succeeded second; + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + var a = executor.submit(() -> compile(engine, input("memory:/a.ili", 2, "first", 20))); + var b = executor.submit(() -> compile(engine, input("memory:/b.ili", 3, "second", 30))); + first = a.get(15, TimeUnit.SECONDS); + second = b.get(15, TimeUnit.SECONDS); + } + assertFalse(timedOut.get()); + assertEquals(0, binding.getCount()); + assertNotEquals(first.analysis().snapshotId(), second.analysis().snapshotId()); + assertDomain(old, "old", "10"); + assertDomain(first, "first", "20"); + assertDomain(second, "second", "30"); + } + + private static CompilationRequest input(String sourceId, long revision, String label, int upper) { + var source = new SourceText(new SourceId(sourceId), new SourceRevision(revision), + "INTERLIS 2.4; MODEL Sample AT \"https://example.org\" VERSION \"1\" = DOMAIN Value = 0.." + + upper + "; END Sample."); + var defaults = CompilerOptions.defaults(); + var metadata = new MetaAttributeOverlay(new OverlaySource("config:/sample", 1), + Target.named("Sample.Value"), Map.of("label", List.of(label))); + var options = new CompilerOptions(defaults.semanticProfile(), defaults.allowedLanguages(), + defaults.warningPolicy(), defaults.budget(), new SemanticOverlays(List.of(metadata))); + return CompilerTestSupport.request(List.of("Sample"), Map.of(source.id(), source), List.of(), options); + } + + private static CompilationOutcome.Succeeded compile(CompilerEngine engine, CompilationRequest request) { + return assertInstanceOf(CompilationOutcome.Succeeded.class, engine.compile(request, CancellationToken.none())); + } + + private static void assertDomain(CompilationOutcome.Succeeded result, String label, String upper) { + var graph = result.schema().semanticGraph(); + var domain = graph.symbols().stream().filter(DomainSymbol.class::isInstance).map(DomainSymbol.class::cast) + .filter(symbol -> symbol.name().value().equals("Value")).findFirst().orElseThrow(); + assertEquals(label, domain.header().metaAttributes().values().get("label").text()); + var type = assertInstanceOf(NumericType.class, graph.type(domain.typeId()).orElseThrow()); + assertEquals(upper, type.range().maximum().orElseThrow().exactValue().toPlainString()); + } +} diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/ViewBindingContractTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/ViewBindingContractTest.java index 9e495102..463a4942 100644 --- a/interlis-compiler/src/test/java/guru/interlis/compiler/ViewBindingContractTest.java +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/ViewBindingContractTest.java @@ -65,6 +65,22 @@ static void rule(String v, String body, String rule) { rule(v, "TOPIC T = CLASS A = x : TEXT; END A; VIEW V PROJECTION OF A; = A := A->x; END V; END T;", "VIEW_ATTRIBUTE_DUPLICATE"); } } + @Test void repeatedAllOfRejectsTheDuplicateAndKeepsTheFirstDerivation() { + for (String v : List.of("2.3", "2.4")) { + var result = assertInstanceOf(CompilationOutcome.Rejected.class, compile(v, + "TOPIC T = CLASS A = x : TEXT; END A; VIEW V PROJECTION OF A; = ALL OF A; ALL OF A; END V; END T;")); + var analysis = result.analysis(); + assertTrue(analysis.diagnostics().stream().anyMatch(d -> d.arguments().toString().contains("VIEW_ATTRIBUTE_DUPLICATE")), + () -> analysis.diagnostics().toString()); + var graph = analysis.semanticGraph(); + var view = graph.symbols().stream().filter(ViewSymbol.class::isInstance).map(ViewSymbol.class::cast).findFirst().orElseThrow(); + assertEquals(1, view.members().size()); + var attribute = (AttributeSymbol) graph.symbol(view.members().getFirst()).orElseThrow(); + var expression = graph.expression(attribute.derivations().getFirst()).orElseThrow(); + var source = analysis.syntaxSnapshots().get(expression.range().source()).source(); + assertEquals(source.text().indexOf("ALL OF A;") + "ALL OF ".length(), expression.range().startOffset()); + } + } @Test void explicitAndTopicViewInheritancePreserveMembers() { for (String v : List.of("2.3", "2.4")) { var result = success(v, """ @@ -113,6 +129,21 @@ static void rule(String v, String body, String rule) { } } + @Test void recursiveInspectionInAClassConstraintRemainsALanguageError() { + for (String version : List.of("2.3", "2.4")) { + rule(version, """ + TOPIC T = + STRUCTURE Child = value : TEXT; END Child; + CLASS A = + children : BAG OF Child; + MANDATORY CONSTRAINT INTERLIS.elementCount(INSPECTION I) > 0; + END A; + VIEW I INSPECTION OF child~A->children; = END I; + END T; + """, "VIEW_CYCLE"); + } + } + @Test void structureInspectionBindsElementAndParentRatherThanRootClass() { for (String v : List.of("2.3", "2.4")) { var result = success(v, """ diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/internal/ReferenceBindingsTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/internal/ReferenceBindingsTest.java new file mode 100644 index 00000000..5022f517 --- /dev/null +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/internal/ReferenceBindingsTest.java @@ -0,0 +1,83 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.execution.CancellationToken; +import guru.interlis.model.BindingResolution; +import guru.interlis.model.LanguageVersion; +import guru.interlis.model.NameReference; +import guru.interlis.model.Origin; +import guru.interlis.model.ReferenceId; +import guru.interlis.model.ReferenceSegment; +import guru.interlis.model.ReferenceUsage; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.UnresolvedReason; +import guru.interlis.source.SourceId; +import guru.interlis.source.SourceRange; +import guru.interlis.source.SourceRevision; +import guru.interlis.syntax.NameSyntax; +import guru.interlis.syntax.SourceMetadata; +import guru.interlis.syntax.SyntaxNodeId; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class ReferenceBindingsTest { + @Test + void deduplicatesOnlyTheSameOwnerRangeUsageAndResolutionBeforeAllocating() { + var source = new SourceId("memory:/references.ili"); + var range = new SourceRange(source, SourceRevision.initial(), 2, 3); + var name = new NameSyntax(new SyntaxNodeId("name"), range, + List.of(new NameSyntax.NameSegmentSyntax("X", range))); + var owner = owner("owner", name); + var target = new SymbolId("target"); + var other = new SymbolId("other-target"); + var declarations = new IndexedDeclarations(List.of(), Map.of(), Map.of(), Map.of(), Map.of()); + var recorded = new ArrayList(); + var bindings = new ReferenceBindings(declarations, new SemanticBuildState(), + new SemanticWork(CancellationToken.none(), 10, 0), recorded::add, PredefinedModels::id); + + bindings.recordReference(owner, name, target, ReferenceUsage.EXPRESSION); + bindings.recordEnumReference(owner.id(), range, List.of(new ReferenceSegment("X", range, Optional.of(target))), target); + bindings.recordReference(owner, name, target, ReferenceUsage.TYPE_USE); + bindings.recordReference(owner, name, other, ReferenceUsage.EXPRESSION); + bindings.recordReference(owner("other-owner", name), name, target, ReferenceUsage.EXPRESSION); + + var shifted = new SourceRange(source, SourceRevision.initial(), 4, 5); + var secondName = new NameSyntax(new SyntaxNodeId("other-name"), shifted, + List.of(new NameSyntax.NameSegmentSyntax("X", shifted))); + bindings.recordReference(owner, secondName, target, ReferenceUsage.EXPRESSION); + bindings.recordReference(owner, secondName, null, ReferenceUsage.EXPRESSION); + bindings.recordReference(owner, secondName, null, ReferenceUsage.EXPRESSION); + var candidates = List.of(other, target); + var ambiguous = new NameResolver.Resolution(NameResolver.Status.AMBIGUOUS, Optional.empty(), candidates); + bindings.recordResolution(owner, secondName, ambiguous, ReferenceUsage.EXPRESSION); + bindings.recordResolution(owner, secondName, ambiguous, ReferenceUsage.EXPRESSION); + var differentCandidates = List.of(other, new SymbolId("third-target")); + bindings.recordResolution(owner, secondName, + new NameResolver.Resolution(NameResolver.Status.AMBIGUOUS, Optional.empty(), differentCandidates), + ReferenceUsage.EXPRESSION); + assertEquals(List.of("ref-1", "ref-2", "ref-3", "ref-4", "ref-5", "ref-6", "ref-7", "ref-8"), + recorded.stream().map(NameReference::id).map(ReferenceId::value).toList()); + assertEquals(List.of(new ReferenceSegment("X", range, Optional.of(target))), recorded.getFirst().segments()); + assertEquals(new BindingResolution.BoundTarget(other), recorded.get(2).resolution()); + assertEquals(shifted, recorded.get(4).range()); + assertEquals(new BindingResolution.UnresolvedTarget(UnresolvedReason.NAME_NOT_FOUND, List.of()), + recorded.get(5).resolution()); + assertEquals(new BindingResolution.UnresolvedTarget(UnresolvedReason.AMBIGUOUS_NAME, candidates), + recorded.get(6).resolution()); + assertEquals(new BindingResolution.UnresolvedTarget(UnresolvedReason.AMBIGUOUS_NAME, differentCandidates), + recorded.getLast().resolution()); + } + + private static DeclInfo owner(String id, NameSyntax name) { + return new DeclInfo(new SymbolId(id), SymbolKind.CLASS, id, Optional.empty(), 0, + new Origin.DeclaredOrigin(name.range()), Optional.of(name.range()), LanguageVersion.ILI24, + Set.of(), name, name.range().source(), id, Optional.empty(), SourceMetadata.empty()); + } +} diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/internal/SemanticBuildBoundaryTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/internal/SemanticBuildBoundaryTest.java new file mode 100644 index 00000000..88fbc5bb --- /dev/null +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/internal/SemanticBuildBoundaryTest.java @@ -0,0 +1,39 @@ +package guru.interlis.compiler.internal; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SemanticBuildBoundaryTest { + @Test + void onlyTheCoordinatorOwnsTheWholeBuild() { + var binders = List.of(NameBindings.class, ReferenceBindings.class, TranslationBindings.class, + TypeBindings.class, FormattedTypeBindings.class, GenericBindings.class, OidBindings.class, + UnitBindings.class, ExpressionBindings.class, ObjectPathBindings.class, ConstraintBindings.class, + InheritanceBindings.class, DeclarationBindings.class, TopicBindings.class, GraphicBuilder.class, + ViewBuilder.class, SegmentContextualizer.class, MetadataBindings.class, FunctionBindings.class, + StructureBindings.class, PredefinedTemporal.class, PredefinedModels.class, LineForms.class, + NumericSubdivisions.class, ExactUnitFactors.class, OverlayBindings.class, SemanticDiagnostics.class); + for (var binder : binders) { + assertFalse(Arrays.stream(binder.getDeclaredFields()).anyMatch(field -> + field.getGenericType().getTypeName().contains(GraphBuilder.class.getName())), binder.getName()); + assertFalse(Arrays.stream(binder.getDeclaredConstructors()).flatMap(ctor -> Arrays.stream(ctor.getGenericParameterTypes())) + .anyMatch(type -> type.getTypeName().contains(GraphBuilder.class.getName())), binder.getName()); + assertFalse(Arrays.stream(binder.getDeclaredMethods()).flatMap(method -> Arrays.stream(method.getGenericParameterTypes())) + .anyMatch(type -> type.getTypeName().contains(GraphBuilder.class.getName())), binder.getName()); + assertFalse(Arrays.stream(binder.getDeclaredFields()).anyMatch(field -> + field.getType() == SemanticBuildState.class), binder.getName()); + } + } + + @Test + void mutableGraphStorageIsPrivate() { + assertTrue(Arrays.stream(SemanticBuildState.class.getDeclaredFields()) + .allMatch(field -> Modifier.isPrivate(field.getModifiers()))); + } +} diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/internal/SemanticBuildStateTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/internal/SemanticBuildStateTest.java new file mode 100644 index 00000000..6d0bf9df --- /dev/null +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/internal/SemanticBuildStateTest.java @@ -0,0 +1,147 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.model.BooleanType; +import guru.interlis.model.ClassSymbol; +import guru.interlis.model.ConstantExpression; +import guru.interlis.model.DeclaredName; +import guru.interlis.model.Documentation; +import guru.interlis.model.EffectiveMember; +import guru.interlis.model.EffectiveMemberOrigin; +import guru.interlis.model.ExpressionTypeState; +import guru.interlis.model.LanguageVersion; +import guru.interlis.model.MetaAttributeSet; +import guru.interlis.model.Origin; +import guru.interlis.model.PendingReason; +import guru.interlis.model.PendingType; +import guru.interlis.model.SnapshotId; +import guru.interlis.model.Symbol; +import guru.interlis.model.SymbolHeader; +import guru.interlis.model.SymbolId; +import guru.interlis.model.SymbolKind; +import guru.interlis.model.TypeId; +import guru.interlis.source.SourceId; +import guru.interlis.source.SourceRevision; +import guru.interlis.source.SourceText; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class SemanticBuildStateTest { + @Test + void definitionAndReplacementPreserveOrderAndRejectUnexpectedWrites() { + var state = new SemanticBuildState(); + var first = declaration("first", "First"); + var second = declaration("second", "Second"); + var replacement = new ClassSymbol(first.header(), Optional.empty(), List.of(), List.of(new SymbolId("constraint"))); + state.defineSymbol(first); + var live = state.symbolsInBuildOrder(); + var earlier = List.copyOf(live); + state.defineSymbol(second); + assertSame(first, state.symbol(first.id())); + assertThrows(IllegalStateException.class, () -> state.defineSymbol(replacement)); + assertThrows(IllegalStateException.class, () -> state.replaceSymbol(declaration("missing", "Missing"))); + assertThrows(IllegalStateException.class, () -> state.replaceSymbol(declaration("first", "Different"))); + state.replaceSymbol(replacement); + assertEquals(List.of(replacement, second), List.copyOf(live)); + assertEquals(List.of(first), earlier); + assertThrows(UnsupportedOperationException.class, live::clear); + } + + @Test + void effectiveMembersAndDeclaredTypeEntriesExposeNoMutableAliases() { + var state = new SemanticBuildState(); + var owner = new SymbolId("owner"); + var member = new EffectiveMember(new SymbolId("member"), owner, List.of(owner), 0, + EffectiveMemberOrigin.LOCAL, false); + var input = new ArrayList<>(List.of(member)); + state.setEffectiveMembers(owner, input); + input.clear(); + assertEquals(List.of(member), state.effectiveMembers(owner)); + assertThrows(UnsupportedOperationException.class, state.effectiveMembers(owner)::clear); + assertThrows(UnsupportedOperationException.class, + () -> state.effectiveMembersInBuildOrder().iterator().next().setValue(List.of())); + + var reserved = state.nextTypeId(); + var other = state.nextTypeId(); + state.bindDeclaredType(owner, reserved); + assertThrows(IllegalStateException.class, () -> state.bindDeclaredType(owner, other)); + assertEquals(reserved, state.declaredType(owner)); + assertThrows(UnsupportedOperationException.class, + () -> state.declaredTypesInBuildOrder().iterator().next().setValue(other)); + } + + @Test + void typeCompletionAndExpressionRetypingKeepTheirAllocatedIdentities() { + var state = new SemanticBuildState(); + var source = new SourceText(new SourceId("memory:/state.ili"), new SourceRevision(1), "PI"); + var range = source.range(0, 2); + var origin = new Origin.DeclaredOrigin(range); + var reserved = state.nextTypeId(); + var other = state.nextTypeId(); + var pending = new PendingType(reserved, origin, PendingReason.NOT_ANALYZED); + var completed = new BooleanType(reserved, origin); + var otherType = new BooleanType(other, origin); + assertThrows(IllegalStateException.class, () -> state.replaceType(completed)); + state.defineType(pending); + assertThrows(IllegalStateException.class, () -> state.defineType(completed)); + state.defineType(otherType); + assertThrows(IllegalStateException.class, + () -> state.replaceType(new BooleanType(reserved, new Origin.DeclaredOrigin(source.range(1, 2))))); + state.replaceType(completed); + + var id = state.nextExpressionId(); + var original = new ConstantExpression(id, other, ExpressionTypeState.RESOLVED, range, List.of(), "PI"); + var retyped = new ConstantExpression(id, reserved, ExpressionTypeState.RESOLVED, range, List.of(), "PI"); + assertThrows(IllegalStateException.class, () -> state.replaceExpression(retyped)); + state.defineExpression(original); + assertThrows(IllegalStateException.class, () -> state.defineExpression(retyped)); + assertThrows(IllegalStateException.class, () -> state.replaceExpression( + new ConstantExpression(id, reserved, ExpressionTypeState.RESOLVED, source.range(1, 2), List.of(), "PI"))); + state.replaceExpression(retyped); + + var graph = state.freeze(new SnapshotId("graph"), List.of(), List.of(), Set.of(), + Map.of(source.id(), source.revision())); + assertEquals(List.of(completed, otherType), List.copyOf(graph.types())); + assertEquals(List.of(retyped), List.copyOf(graph.expressions())); + assertThrows(IllegalStateException.class, () -> state.defineType(pending)); + assertThrows(IllegalStateException.class, () -> state.replaceType(completed)); + assertThrows(IllegalStateException.class, () -> state.defineExpression(original)); + assertThrows(IllegalStateException.class, () -> state.replaceExpression(retyped)); + assertThrows(IllegalStateException.class, state::nextTypeId); + assertThrows(IllegalStateException.class, state::nextTypeSequence); + } + + @Test + void publicationClosesTheStoreAndLeavesOtherRunsIndependent() { + var state = new SemanticBuildState(); + var first = declaration("a", "First"); + state.defineSymbol(first); + var nextRun = new SemanticBuildState(); + assertEquals(state.nextTypeId(), nextRun.nextTypeId()); + assertEquals(state.nextExpressionId(), nextRun.nextExpressionId()); + + var graph = state.freeze(new SnapshotId("first"), List.of(), List.of(), Set.of(), Map.of()); + nextRun.defineSymbol(declaration("a", "Other")); + assertEquals(List.of(first), List.copyOf(graph.symbols())); + assertThrows(UnsupportedOperationException.class, graph.symbols()::clear); + assertThrows(IllegalStateException.class, () -> state.replaceSymbol(declaration("a", "Replaced"))); + assertThrows(IllegalStateException.class, () -> state.bindBase(first.id(), first.id())); + assertThrows(IllegalStateException.class, () -> state.bindDeclaredType(first.id(), new TypeId("late"))); + assertThrows(IllegalStateException.class, state::nextExpressionId); + assertThrows(IllegalStateException.class, + () -> state.freeze(new SnapshotId("second"), List.of(), List.of(), Set.of(), Map.of())); + } + + private static Symbol declaration(String id, String name) { + var header = new SymbolHeader(new SymbolId(id), SymbolKind.CLASS, DeclaredName.declared(name), + Optional.empty(), 0, new Origin.PredefinedOrigin(LanguageVersion.ILI24, name), + Optional.empty(), LanguageVersion.ILI24, Documentation.empty(), MetaAttributeSet.empty(), Set.of(), List.of()); + return new ClassSymbol(header, Optional.empty(), List.of(), List.of()); + } +} diff --git a/interlis-compiler/src/test/java/guru/interlis/compiler/internal/SemanticWorkBudgetTest.java b/interlis-compiler/src/test/java/guru/interlis/compiler/internal/SemanticWorkBudgetTest.java new file mode 100644 index 00000000..bd8c142a --- /dev/null +++ b/interlis-compiler/src/test/java/guru/interlis/compiler/internal/SemanticWorkBudgetTest.java @@ -0,0 +1,36 @@ +package guru.interlis.compiler.internal; + +import guru.interlis.execution.CancellationReason; +import guru.interlis.execution.CancellationToken; +import guru.interlis.execution.OperationCancelledException; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +class SemanticWorkBudgetTest { + @Test + void remainingBudgetIncludesOverlayWorkAndAllowsTheLastUnit() { + var work = new SemanticWork(CancellationToken.none(), 4, 2); + work.checkpoint(); + work.checkpoint(); + var exceeded = assertThrows(AnalysisBudgetExceededException.class, work::checkpoint); + assertEquals("maxSemanticWorkUnits", exceeded.resource()); + assertEquals(4, exceeded.limit()); + } + + @Test + void cancellationIsCheckedBeforeChargingTheNextUnit() { + var cancelled = new AtomicBoolean(true); + var work = new SemanticWork(cancelled::get, 2, 1); + var aborted = assertThrows(OperationCancelledException.class, work::checkpoint); + assertEquals(CancellationReason.USER_REQUESTED, aborted.reason()); + cancelled.set(false); + work.checkpoint(); + assertThrows(AnalysisBudgetExceededException.class, work::checkpoint); + + var exhausted = new SemanticWork(CancellationToken.requested(CancellationReason.USER_REQUESTED), 1, 1); + assertThrows(OperationCancelledException.class, exhausted::checkpoint); + } +} diff --git a/interlis-lsp/src/main/java/guru/interlis/lsp/AnalysisScheduler.java b/interlis-lsp/src/main/java/guru/interlis/lsp/AnalysisScheduler.java index 68b57100..a2a1774a 100644 --- a/interlis-lsp/src/main/java/guru/interlis/lsp/AnalysisScheduler.java +++ b/interlis-lsp/src/main/java/guru/interlis/lsp/AnalysisScheduler.java @@ -2,6 +2,7 @@ import guru.interlis.compiler.*; import guru.interlis.execution.*; +import guru.interlis.repository.RepositoryResolutionOutcome; import guru.interlis.workspace.*; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; @@ -20,11 +21,11 @@ final class AnalysisScheduler implements AutoCloseable { private final Workspace workspace; private final RepositoryHostCoordinator repositories; - private Consumer repositoryResult=ignored -> { }; + private Consumer repositoryPrepared = ignored -> { }; private Predicate localSource=ignored -> true; private final CompilerEngine engine; private final CompilerServices services; - private final Consumer accepted; + private final BiConsumer> accepted; private final Consumer operations; private final ThreadPoolExecutor owner = new ThreadPoolExecutor(1,1,0,TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(256),r -> daemon(r,"interlis-lsp-owner")); @@ -49,7 +50,8 @@ final class AnalysisScheduler implements AutoCloseable { private final Set> compiles = new LinkedHashSet<>(); AnalysisScheduler(Workspace workspace, CompilerEngine engine, CompilerServices services, - Consumer accepted, Consumer operations) { + BiConsumer> accepted, + Consumer operations) { this.workspace = Objects.requireNonNull(workspace); this.engine = Objects.requireNonNull(engine); this.services = Objects.requireNonNull(services); @@ -59,12 +61,15 @@ final class AnalysisScheduler implements AutoCloseable { } void repositorySettings(RepositorySettings settings) { requireOwner(); repositories.reconfigure(settings); } - void repositoryResult(Consumer listener, - Predicate local) { repositoryResult=listener;localSource=local; } + void repositorySources(Consumer listener, + Predicate local) { repositoryPrepared=listener;localSource=local; } /** Short ordered host action; never waits for a compiler worker. */ T onOwner(Supplier action) { - if (Thread.currentThread() == ownerThread) return action.get(); + if (Thread.currentThread() == ownerThread) { + if (closing.get()) throw stopped(); + return action.get(); + } var result = new CompletableFuture(); enqueue(() -> { try { result.complete(action.get()); } @@ -77,6 +82,13 @@ T onOwner(Supplier action) { } } + /** Delivers completion of admitted work without competing for a new request slot. */ + void completeOnOwner(Runnable action) { + if (Thread.currentThread() == ownerThread) { + if (!closing.get()) action.run(); + } else enqueue(action, null); + } + /** Invalidates even when only client generation/version changed, without a core text change. */ void invalidate() { requireOwner(); @@ -85,8 +97,9 @@ void invalidate() { captureFailure=null; running.forEach(job -> job.cancelled.set(true)); pending = null; - for (var query : List.copyOf(queries)) query.result.completeExceptionally(changed()); + for (var result : List.copyOf(activeQueries)) result.completeExceptionally(changed()); queries.clear(); + for (var result : List.copyOf(compiles)) result.completeExceptionally(changed()); } /** Called on owner after the complete source/configuration update has been accepted. */ @@ -112,38 +125,61 @@ void schedule() { /** Captures the query generation, promotes current work and propagates request cancellation. */ CompletableFuture requestNow(Function> action) { var result = new CompletableFuture(); - result.whenComplete((ignored,failure) -> { - Runnable cleanup=() -> { queries.removeIf(query -> query.result==result); activeQueries.remove(result); }; - if(Thread.currentThread()==ownerThread) cleanup.run(); else enqueue(cleanup,null); - }); enqueue(() -> { if(result.isDone()) return; - activeQueries.add(result); if(current && !workspace.currentRevision().equals(acceptedRevision)) schedule(); if(captureFailure!=null) { result.completeExceptionally(captureFailure); return; } if (current || workspace.state().sourceRevisions().isEmpty()) { + trackQuery(result); invoke(new PendingQuery<>(generation, action, result)); } else { - if (running.stream().noneMatch(job -> job.generation == generation) && pending == null) schedule(); - if(captureFailure!=null) { result.completeExceptionally(captureFailure); return; } + if (!analysisScheduled()) { + schedule(); + if (captureFailure != null) { + result.completeExceptionally(captureFailure); + return; + } + if (!analysisScheduled()) { + result.completeExceptionally(changed()); + return; + } + } if (queries.size() >= 128) { result.completeExceptionally(error(ResponseErrorCode.RequestFailed, "language request queue is full")); return; } queries.add(new PendingQuery<>(generation, action, result)); + trackQuery(result); } }, result); return result; } + private void trackQuery(CompletableFuture result) { + activeQueries.add(result); + result.whenComplete((ignored, failure) -> completeOnOwner(() -> { + queries.removeIf(query -> query.result == result); + activeQueries.remove(result); + })); + } + + private boolean analysisScheduled() { + return pending != null || running.stream().anyMatch(job -> job.generation == generation); + } + /** Runs a pure operation over a host snapshot, outside owner and workspace mutation. */ CompletableFuture detached(CancellationToken token,Supplier> operation) { requireOwner(); var result=new CompletableFuture(); try { language.execute(() -> { try { + if (result.isDone()) return; token.throwIfCancellationRequested(); - operation.get().whenComplete((value,failure) -> { + var active = operation.get(); + result.whenComplete((ignored, failure) -> { + if (result.isCancelled()) active.cancel(false); + }); + active.whenComplete((value,failure) -> { if(failure!=null) result.completeExceptionally(LspFailures.response(failure,operations)); else result.complete(value); }); @@ -162,6 +198,7 @@ CompletableFuture detached(CancellationToken token,Supplier compileRoots(List selection,Runnable validate) { var result=new CompletableFuture(); enqueue(() -> { + if (result.isDone()) return; validate.run(); var roots=selection==null?workspace.state().sourceRevisions().keySet().stream().filter(localSource) .map(ModelSelector.BySource::new).toList():List.copyOf(selection); @@ -175,25 +212,24 @@ CompletableFuture compileRoots(List selection public CancellationReason reason() { return closing.get()?CancellationReason.HOST_SHUTDOWN: generation!=initialGeneration?CancellationReason.SUPERSEDED:CancellationReason.USER_REQUESTED; } }; - var preparation=!repositories.needsResolution(initial)?CompletableFuture.completedFuture(null):repositories.resolve(workspace,initial,workspace.configuration(),token); + var preparation=!repositories.needsResolution(initial)?CompletableFuture.completedFuture(null):repositories.resolve(workspace,initial,workspace.configuration(),token); preparation.whenComplete((prepared,failure) -> enqueue(() -> { if(result.isDone()) return; if(failure!=null) { result.completeExceptionally(LspFailures.response(failure,operations));return; } if(initialGeneration!=generation || !workspace.currentRevision().equals(initial.workspaceRevision())) { result.completeExceptionally(changed());return; } - if(prepared instanceof guru.interlis.repository.RepositoryResolutionOutcome.Cancelled cancelled) { + if(prepared instanceof RepositoryResolutionOutcome.Cancelled cancelled) { result.complete(new CompilationOutcome.Cancelled(Optional.empty(),cancelled.reason()));return; } - if(prepared instanceof guru.interlis.repository.RepositoryResolutionOutcome.Stale) { result.completeExceptionally(changed());return; } - if(prepared instanceof guru.interlis.repository.RepositoryResolutionOutcome.Rejected rejected) { - repositoryResult.accept(rejected); + if(prepared instanceof RepositoryResolutionOutcome.Stale) { result.completeExceptionally(changed());return; } + if(prepared instanceof RepositoryResolutionOutcome.Rejected rejected) { // A purely local ambiguity already belongs to this exact capture. // Preserve the existing compile gate's AMBIGUOUS_INPUT outcome. boolean localAmbiguity=rejected.problems().stream().allMatch(p -> p.reason()==guru.interlis.repository.RepositoryProblem.Reason.AMBIGUOUS) && rejected.sources().keySet().stream().allMatch(initial.request().sources().sources()::containsKey); if(!localAmbiguity) { result.completeExceptionally(error(ResponseErrorCode.RequestFailed,"repository closure rejected: "+rejected.problems().stream().map(p -> p.loadReason().map(Enum::name).orElse(p.reason().name())).distinct().toList()));return; } } - if(prepared instanceof guru.interlis.repository.RepositoryResolutionOutcome.Prepared success) { - success.update().apply(workspace);repositoryResult.accept(prepared); + if(prepared instanceof RepositoryResolutionOutcome.Prepared success) { + success.update().apply(workspace);repositoryPrepared.accept(success); } var capture=workspace.capture(new AnalysisRequest(workspace.currentRevision(),roots),token); try { compiler.execute(() -> { @@ -203,6 +239,7 @@ public CancellationReason reason() { return closing.get()?CancellationReason.HOS catch(RuntimeException problem) { result.completeExceptionally(LspFailures.response(problem,operations));return; } var outcome=value; enqueue(() -> { + if (result.isDone()) return; if(initialGeneration!=generation || !workspace.currentRevision().equals(capture.workspaceRevision())) { result.completeExceptionally(changed());return; } if(outcome instanceof CompilationOutcome.Succeeded success) workspace.acceptAnalysis(capture,success.analysis()); else if(outcome instanceof CompilationOutcome.Rejected rejected) workspace.acceptAnalysis(capture,rejected.analysis()); @@ -217,21 +254,23 @@ public CancellationReason reason() { return closing.get()?CancellationReason.HOS private void launch(Job job) { running.add(job); - if(!repositories.needsResolution(job.capture)) { repositoryResult.accept(null);analyze(job);return; } + if(!repositories.needsResolution(job.capture)) { analyze(job);return; } repositories.resolve(workspace,job.capture,workspace.configuration(),job.token()).whenComplete((outcome,failure) -> { if(closed) return; enqueue(() -> { if(job.generation!=generation || job.cancelled.get()) { finish(job,null,new OperationCancelledException(CancellationReason.SUPERSEDED)); return; } if(failure!=null) { finish(job,null,LspFailures.response(failure,operations)); return; } - if(outcome instanceof guru.interlis.repository.RepositoryResolutionOutcome.Cancelled cancelled) { + if(outcome instanceof RepositoryResolutionOutcome.Cancelled cancelled) { finish(job,null,new OperationCancelledException(cancelled.reason()));return; } - if(outcome instanceof guru.interlis.repository.RepositoryResolutionOutcome.Prepared prepared) { + if(outcome instanceof RepositoryResolutionOutcome.Prepared prepared) { try { prepared.update().apply(workspace); } catch(UpdateRejectedException stale) { finish(job,null,changed());return; } - repositoryResult.accept(outcome); + repositoryPrepared.accept(prepared); job.capture=workspace.capture(new AnalysisRequest(workspace.currentRevision(),job.capture.request().roots()),job.token()); - } else repositoryResult.accept(outcome); + } else if (outcome instanceof RepositoryResolutionOutcome.Rejected rejected) { + job.repositoryFailure = Optional.of(rejected); + } analyze(job); },null); }); @@ -257,7 +296,7 @@ private void finish(Job job, AnalysisSnapshot analysis, Throwable failure) { if (failure == null && workspace.acceptAnalysis(job.capture, analysis) == AnalysisAcceptance.ACCEPTED) { current = true; acceptedRevision = job.capture.workspaceRevision(); - accepted.accept(analysis); + accepted.accept(analysis, job.repositoryFailure); for (var query : List.copyOf(queries)) invoke(query); queries.clear(); } else { @@ -289,7 +328,12 @@ public CancellationReason reason() { return closing.get()?CancellationReason.HOS }; try { token.throwIfCancellationRequested(); - query.action.apply(token).whenComplete((value, failure) -> { + var active = query.action.apply(token); + query.result.whenComplete((ignored, failure) -> { + if (failure != null) active.cancel(false); + }); + active.whenComplete((value, failure) -> { + if (query.result.isDone()) return; enqueue(() -> { if(query.generation!=generation || !workspace.currentRevision().equals(revision) || !workspace.configuration().fullFingerprint().equals(configuration)) query.result.completeExceptionally(changed()); @@ -309,7 +353,10 @@ private ResponseErrorException internal(Throwable failure) { private void enqueue(Runnable action, CompletableFuture result) { Runnable task=() -> { ownerThread = Thread.currentThread(); - if(closed) { if(result!=null) result.completeExceptionally(error(ResponseErrorCode.RequestFailed,"server is closed")); return; } + if (closing.get()) { + if (result != null) result.completeExceptionally(stopped()); + return; + } try { action.run(); } catch(RuntimeException failure) { var response=LspFailures.response(failure,operations); @@ -317,8 +364,8 @@ private void enqueue(Runnable action, CompletableFuture result) { else { var waiting=List.copyOf(queries); queries.clear(); - invalidate(); for(var query:waiting) query.result.completeExceptionally(response); + invalidate(); } } }; @@ -326,10 +373,14 @@ private void enqueue(Runnable action, CompletableFuture result) { if (closed || closing.get()) throw new RejectedExecutionException(); owner.execute(task); } catch (RejectedExecutionException unavailable) { - if(result!=null) result.completeExceptionally(error(ResponseErrorCode.RequestFailed,"owner queue is full or closed")); + if (result != null) result.completeExceptionally(closing.get() ? stopped() + : error(ResponseErrorCode.RequestFailed, "owner queue is full")); else if(!closed && !closing.get()) { - try { owner.getQueue().put(task); } - catch(InterruptedException interrupted) { Thread.currentThread().interrupt(); } + if (Thread.currentThread() == ownerThread) task.run(); + else { + try { owner.getQueue().put(task); } + catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); } + } } } } @@ -346,8 +397,13 @@ void close(Runnable hostCleanup) { if(closing.compareAndSet(false,true)) { Runnable shutdown=() -> { ownerThread=Thread.currentThread(); - try { hostCleanup.run(); invalidate(); List.copyOf(activeQueries).forEach(result -> result.cancel(false)); activeQueries.clear(); - running.clear(); compiles.forEach(result -> result.cancel(false)); compiles.clear(); } + try { + hostCleanup.run(); + List.copyOf(activeQueries).forEach(result -> result.cancel(false)); + List.copyOf(compiles).forEach(result -> result.cancel(false)); + invalidate(); + activeQueries.clear(); running.clear(); compiles.clear(); + } finally { closed=true; repositories.close(); compiler.shutdownNow(); workers.shutdownNow(); language.shutdownNow(); owner.shutdown(); terminated.complete(null); } @@ -361,13 +417,14 @@ void close(Runnable hostCleanup) { } } } - terminated.join(); + if (Thread.currentThread() != ownerThread) terminated.join(); } private static Thread daemon(Runnable runnable, String name) { var thread = new Thread(runnable, name); thread.setDaemon(true); return thread; } private static ResponseErrorException changed() { return error(ResponseErrorCode.ContentModified, "CONTENT_MODIFIED: document or configuration changed"); } + private static ResponseErrorException stopped() { return error(ResponseErrorCode.RequestCancelled, "server is closing"); } private static ResponseErrorException error(ResponseErrorCode code, String message) { return new ResponseErrorException(new ResponseError(code, message, null)); } @@ -375,6 +432,7 @@ private record PendingQuery(long generation, Function result) { } private final class Job { final long generation; WorkspaceCapture capture; final AtomicBoolean cancelled = new AtomicBoolean(); + Optional repositoryFailure = Optional.empty(); Job(long generation, WorkspaceCapture capture) { this.generation = generation; this.capture = capture; } CancellationToken token() { return new CancellationToken() { diff --git a/interlis-lsp/src/main/java/guru/interlis/lsp/ExternalSourceRegistry.java b/interlis-lsp/src/main/java/guru/interlis/lsp/ExternalSourceRegistry.java index 32157b52..6993e053 100644 --- a/interlis-lsp/src/main/java/guru/interlis/lsp/ExternalSourceRegistry.java +++ b/interlis-lsp/src/main/java/guru/interlis/lsp/ExternalSourceRegistry.java @@ -25,6 +25,7 @@ void register(SourceText source,long limit) { handles.put(uri,entry);current.put(source.id(),entry);units+=source.length(); } boolean contains(SourceId id) { return current.containsKey(id); } + Optional currentUri(SourceId id) { return Optional.ofNullable(current.get(id)).map(Entry::uri); } Optional lookup(String uri) { return Optional.ofNullable(handles.get(uri)); } Map uris() { var result=new LinkedHashMap();current.forEach((id,e) -> result.put(id,e.uri));return Map.copyOf(result); } Entry read(String uri,String revision) { diff --git a/interlis-lsp/src/main/java/guru/interlis/lsp/InterlisLanguageServer.java b/interlis-lsp/src/main/java/guru/interlis/lsp/InterlisLanguageServer.java index 6261da45..f64821a8 100644 --- a/interlis-lsp/src/main/java/guru/interlis/lsp/InterlisLanguageServer.java +++ b/interlis-lsp/src/main/java/guru/interlis/lsp/InterlisLanguageServer.java @@ -134,16 +134,16 @@ public CompletableFuture initialize(InitializeParams params) { public CompletableFuture shutdown() { try { requireReady(); } catch(org.eclipse.lsp4j.jsonrpc.ResponseErrorException unavailable) { return CompletableFuture.failedFuture(unavailable); } shutdown = true; - workspaceService.close(); textDocuments.close(); + workspaceService.close(); workspace.close(); return CompletableFuture.completedFuture(null); } @Override public void exit() { - workspaceService.close(); textDocuments.close(); + workspaceService.close(); workspace.close(); processExit.accept(shutdown?0:1); } diff --git a/interlis-lsp/src/main/java/guru/interlis/lsp/InterlisTextDocumentService.java b/interlis-lsp/src/main/java/guru/interlis/lsp/InterlisTextDocumentService.java index 3e3941ed..06e7ca4d 100644 --- a/interlis-lsp/src/main/java/guru/interlis/lsp/InterlisTextDocumentService.java +++ b/interlis-lsp/src/main/java/guru/interlis/lsp/InterlisTextDocumentService.java @@ -16,8 +16,7 @@ import guru.interlis.format.ModelFormatters; import guru.interlis.compiler.query.LanguageQueries; import guru.interlis.diagnostics.Diagnostic; -import guru.interlis.diagnostics.DiagnosticArgument; -import guru.interlis.diagnostics.Severity; +import guru.interlis.repository.RepositoryResolutionOutcome; import guru.interlis.execution.CancellationToken; import guru.interlis.model.NamePrefix; import guru.interlis.model.Symbol; @@ -32,6 +31,7 @@ import guru.interlis.projection.DiagramSelection; import guru.interlis.source.SourceId; import guru.interlis.source.SourceRange; +import guru.interlis.source.SourceRevision; import guru.interlis.source.SourceText; import guru.interlis.syntax.SyntaxToken; import guru.interlis.workspace.AnalysisRequest; @@ -39,6 +39,8 @@ import guru.interlis.workspace.SourceExpectation; import guru.interlis.workspace.SourceUpdate; import guru.interlis.workspace.Workspace; +import guru.interlis.workspace.WorkspaceState; +import guru.interlis.lsp.LspDocumentRegistry.Synchronized; import org.eclipse.lsp4j.CodeAction; import org.eclipse.lsp4j.CodeActionParams; import org.eclipse.lsp4j.Command; @@ -47,7 +49,6 @@ import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.DefinitionParams; -import org.eclipse.lsp4j.DiagnosticSeverity; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; @@ -75,7 +76,6 @@ import org.eclipse.lsp4j.MarkupKind; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.PublishDiagnosticsParams; -import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolKind; import org.eclipse.lsp4j.TextDocumentContentChangeEvent; import org.eclipse.lsp4j.TextDocumentIdentifier; @@ -89,10 +89,12 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -103,34 +105,22 @@ final class InterlisTextDocumentService implements TextDocumentService { private guru.interlis.workspace.WorkspaceConfiguration userConfiguration; private RepositorySettings repositorySettings=RepositorySettings.defaults(); private guru.interlis.diagnostics.DiagnosticLanguage localeLanguage=guru.interlis.diagnostics.DiagnosticLanguage.EN; - private guru.interlis.repository.RepositoryResolutionOutcome.Rejected repositoryFailure; - private final java.util.Set publishedExternal=new java.util.HashSet<>(); + private record ClosedPublication(String uri, SourceRevision revision) { } + + private final Map publishedClosed = new LinkedHashMap<>(); private final LspDiagnosticMapper diagnosticMapper=new LspDiagnosticMapper(); private guru.interlis.diagnostics.DiagnosticLanguage diagnosticLanguage=guru.interlis.diagnostics.DiagnosticLanguage.EN; - private final Map documents = new LinkedHashMap<>(); - private final java.util.Set unsynchronized = new java.util.HashSet<>(); - private record RejectedOpen(SourceId sourceId,int version) { } - private final Map rejectedOpens=new LinkedHashMap<>(); + private final LspDocumentRegistry documents = new LspDocumentRegistry(); private volatile java.util.function.Consumer operations = ignored -> { }; private final AnalysisScheduler scheduler; - private final Map closedFiles = new LinkedHashMap<>(); - private final Map fileIdentities=new LinkedHashMap<>(); - private final Map fileKeys=new LinkedHashMap<>(); - private final java.util.ArrayDeque documentOperations=new java.util.ArrayDeque<>(); - private boolean resolvingOpen; - private long nextOpen; + private final LspInputReadiness readiness; + private final LspFileIdentityRegistry fileIdentities = new LspFileIdentityRegistry(); private java.util.function.BiFunction,CompletableFuture> identifyFile= (uri,known) -> CompletableFuture.completedFuture(new ProjectSourceLoader.IdentityMatch(Optional.empty(),Optional.empty())); private volatile java.util.function.Consumer externalReload; private java.util.function.BiFunction,CancellationToken,CompletableFuture> verifyFiles = (files,token) -> failed(ResponseErrorCode.RequestFailed,"project file service is not initialized"); private boolean versionedEdits; - private long documentGeneration; - private final Map pendingFiles=new LinkedHashMap<>(); - private CompletableFuture filesReady=CompletableFuture.completedFuture(null); - private Throwable filesFailure; - private final java.util.concurrent.atomic.AtomicInteger waitingRequests=new java.util.concurrent.atomic.AtomicInteger(); - private final java.util.Set> fileWaiters=java.util.concurrent.ConcurrentHashMap.newKeySet(); private final LanguageQueries queries = LanguageQueries.create(); private final LspDiagramService diagrams=new LspDiagramService(); private volatile java.util.function.Consumer analysisReady=ignored -> { }; @@ -146,7 +136,8 @@ private record RejectedOpen(SourceId sourceId,int version) { } this.userConfiguration=workspace.configuration(); scheduler = new AnalysisScheduler(workspace, engine, services, this::accepted, message -> operations.accept(message)); - scheduler.repositoryResult(this::repositoryResolved,id -> !externalSources.contains(id)); + scheduler.repositorySources(this::repositoryResolved, id -> !externalSources.contains(id)); + readiness = new LspInputReadiness(documents::generation, scheduler::completeOnOwner, this::operation); } void sink(DiagnosticSink sink) { @@ -158,17 +149,22 @@ void operationSink(java.util.function.Consumer sink) { } void close() { scheduler.close(() -> { - fileWaiters.forEach(result -> result.completeExceptionally(LspFailures.error(ResponseErrorCode.RequestCancelled,"server is closing"))); - fileWaiters.clear(); - filesReady.completeExceptionally(LspFailures.error(ResponseErrorCode.RequestCancelled,"server is closing")); - externalSources.close(); publishedExternal.clear(); + readiness.cancelPendingOnClose(); + var retired = new LinkedHashSet<>(documents.synchronizedDocuments().keySet()); + retireClosedDiagnostics(retired); + externalSources.close(); diagrams.close();analysisReady=ignored -> { }; - pendingFiles.clear(); closedFiles.clear(); fileIdentities.clear(); fileKeys.clear(); documentOperations.clear(); documents.clear(); rejectedOpens.clear(); unsynchronized.clear(); + fileIdentities.clear(); documents.clear(); + retired.forEach(this::clearDocumentDiagnostics); }); } CompletableFuture compileWorkspace() { try { requestGuard.run(); } catch(ResponseErrorException invalid) { return CompletableFuture.failedFuture(invalid); } - return afterFiles(() -> unsynchronized.isEmpty()?scheduler.compile():failed(ResponseErrorCode.ContentModified,"full document resynchronization is required")); + return afterFiles(() -> { + if (documents.needsResync()) return failed(ResponseErrorCode.ContentModified, "full document resynchronization is required"); + long generation = documents.generation(); + return scheduler.compileRoots(null, () -> requireInputGeneration(generation)); + }); } CompletableFuture compileModel(CompileModelParams params) { @@ -181,49 +177,52 @@ CompletableFuture compileModel(CompileModelParams params) { var captured=copyParams(params,CompileModelParams.class); var choosing=requestReady((view,token) -> { var state=requireState(view,new Position(0,0),captured.textDocument()); - if(state.lspVersion!=captured.textDocument().getVersion()) return failed(ResponseErrorCode.ContentModified,"compile document version changed"); - if(!editable(view,state.sourceId)) return failed(ResponseErrorCode.RequestFailed,"compile current requires a local editable source"); - var models=state.analysis.semanticGraph().declarationsInOrder().stream() + if(state.lspVersion()!=captured.textDocument().getVersion()) return failed(ResponseErrorCode.ContentModified,"compile document version changed"); + if(!editable(view,state.sourceId())) return failed(ResponseErrorCode.RequestFailed,"compile current requires a local editable source"); + var models=state.analysis().semanticGraph().declarationsInOrder().stream() .filter(guru.interlis.model.ModelSymbol.class::isInstance).map(guru.interlis.model.ModelSymbol.class::cast) - .filter(m -> m.nameRange().map(r -> r.source().equals(state.sourceId)).orElse(false)) + .filter(m -> m.nameRange().map(r -> r.source().equals(state.sourceId())).orElse(false)) .map(m -> new CompileReport.ModelChoice(m.name().value(),m.modelVersion())).toList(); - if(captured.modelName()==null && models.size()>1) return CompletableFuture.completedFuture(new CompileSelection(List.of(),models)); - List roots=List.of(new guru.interlis.compiler.ModelSelector.BySource(state.sourceId)); + if(captured.modelName()==null && models.size()>1) return CompletableFuture.completedFuture(new CompileSelection(List.of(),models,view.documentGeneration())); + List roots=List.of(new guru.interlis.compiler.ModelSelector.BySource(state.sourceId())); if(captured.modelName()!=null) { var match=models.stream().filter(m -> m.modelName().equals(captured.modelName()) && (captured.modelVersion()==null || m.modelVersion().equals(captured.modelVersion()))).toList(); if(match.size()!=1) return failed(ResponseErrorCode.InvalidParams,"model selection is missing or ambiguous in current document"); - roots=List.of(new guru.interlis.compiler.ModelSelector.ByName(match.getFirst().modelName(),Optional.of(match.getFirst().modelVersion()),Optional.of(state.sourceId))); + roots=List.of(new guru.interlis.compiler.ModelSelector.ByName(match.getFirst().modelName(),Optional.of(match.getFirst().modelVersion()),Optional.of(state.sourceId()))); } - return CompletableFuture.completedFuture(new CompileSelection(roots,List.of())); + return CompletableFuture.completedFuture(new CompileSelection(roots,List.of(),view.documentGeneration())); }); return LspFutures.compose(choosing,selection -> selection.roots().isEmpty()?CompletableFuture.completedFuture( new CompileReport("selectionRequired",selection.choices(),CompileReport.Summary.empty())): - LspFutures.map(afterFiles(() -> scheduler.compileRoots(selection.roots(),() -> { - var open=documents.get(captured.textDocument().getUri()); - if(open==null || open.lspVersion!=captured.textDocument().getVersion() || !unsynchronized.isEmpty()) throw LspFailures.error(ResponseErrorCode.ContentModified,"compile document version changed"); - })),this::compileReport,operations),operations); + LspFutures.map(afterFiles(() -> scheduler.compileRoots(selection.roots(), () -> + requireSelectedDocument(captured.textDocument(), selection.documentGeneration(), "compile document version changed"))), + this::compileReport,operations),operations); } CompletableFuture exportArtifact(ExportArtifactParams params) { if(params==null||params.protocolVersion()!=1||params.textDocument()==null||params.textDocument().getVersion()==null||!LspExportService.FORMATS.contains(params.format()))return failed(ResponseErrorCode.InvalidParams,"export requires protocol 1, a format and an exact document version"); var p=copyParams(params,ExportArtifactParams.class); try {LspExportService.options(p);}catch(RuntimeException invalid){return failed(ResponseErrorCode.InvalidParams,"invalid documentation options");} var selection=requestReady((view,token)->{ - var state=requireState(view,new Position(0,0),p.textDocument());if(state.lspVersion!=p.textDocument().getVersion())return failed(ResponseErrorCode.ContentModified,"export document version changed"); - var models=state.analysis.semanticGraph().symbols().stream().filter(guru.interlis.model.ModelSymbol.class::isInstance).map(guru.interlis.model.ModelSymbol.class::cast).filter(m->m.nameRange().map(r->r.source().equals(state.sourceId)).orElse(false)).toList(); + var state=requireState(view,new Position(0,0),p.textDocument());if(state.lspVersion()!=p.textDocument().getVersion())return failed(ResponseErrorCode.ContentModified,"export document version changed"); + var models=state.analysis().semanticGraph().symbols().stream().filter(guru.interlis.model.ModelSymbol.class::isInstance).map(guru.interlis.model.ModelSymbol.class::cast).filter(m->m.nameRange().map(r->r.source().equals(state.sourceId())).orElse(false)).toList(); if(models.size()>1&&p.modelName()==null)return failed(ResponseErrorCode.InvalidParams,"choose an explicit model for this source"); - List roots=List.of(new guru.interlis.compiler.ModelSelector.BySource(state.sourceId)); - if(p.modelName()!=null){var matching=models.stream().filter(m->m.name().value().equals(p.modelName())&&(p.modelVersion()==null||m.modelVersion().equals(p.modelVersion()))).toList();if(matching.size()!=1)return failed(ResponseErrorCode.InvalidParams,"model selection is unavailable or ambiguous");roots=List.of(new guru.interlis.compiler.ModelSelector.ByName(p.modelName(),Optional.of(matching.getFirst().modelVersion()),Optional.of(state.sourceId)));} - return CompletableFuture.completedFuture(roots); + List roots=List.of(new guru.interlis.compiler.ModelSelector.BySource(state.sourceId())); + if(p.modelName()!=null){var matching=models.stream().filter(m->m.name().value().equals(p.modelName())&&(p.modelVersion()==null||m.modelVersion().equals(p.modelVersion()))).toList();if(matching.size()!=1)return failed(ResponseErrorCode.InvalidParams,"model selection is unavailable or ambiguous");roots=List.of(new guru.interlis.compiler.ModelSelector.ByName(p.modelName(),Optional.of(matching.getFirst().modelVersion()),Optional.of(state.sourceId())));} + return CompletableFuture.completedFuture(new CompileSelection(roots,List.of(),view.documentGeneration())); }); - return LspFutures.compose(selection,roots->LspFutures.compose(afterFiles(()->scheduler.compileRoots(roots,()->{var state=documents.get(p.textDocument().getUri());if(state==null||state.lspVersion!=p.textDocument().getVersion()||!unsynchronized.isEmpty())throw LspFailures.error(ResponseErrorCode.ContentModified,"export source changed");})),outcome->{ + return LspFutures.compose(selection, chosen -> LspFutures.compose(afterFiles(() -> scheduler.compileRoots(chosen.roots(), () -> + requireSelectedDocument(p.textDocument(), chosen.documentGeneration(), "export source changed"))), outcome -> { if(!(outcome instanceof guru.interlis.compiler.CompilationOutcome.Succeeded success))return failed(outcome instanceof guru.interlis.compiler.CompilationOutcome.Cancelled?ResponseErrorCode.RequestCancelled:ResponseErrorCode.RequestFailed,"Documentation export requires a complete valid current model"); - return requestReady((view,token)->{var state=requireState(view,new Position(0,0),p.textDocument());if(state.lspVersion!=p.textDocument().getVersion())return failed(ResponseErrorCode.ContentModified,"export document version changed"); + return requestReady((view,token) -> { + var state=requireState(view,new Position(0,0),p.textDocument()); + if (state.lspVersion() != p.textDocument().getVersion() || view.documentGeneration() != chosen.documentGeneration()) + return failed(ResponseErrorCode.ContentModified,"export document version changed"); var revisions=new LinkedHashMap();var closed=new ArrayList(); for(var e:success.schema().sourceRevisions().entrySet()){var source=view.source(e.getKey()).orElse(null);if(source==null||!source.revision().equals(e.getValue()))return failed(ResponseErrorCode.ContentModified,"export dependency changed");revisions.put(view.uri(e.getKey()),Long.toString(e.getValue().value()));var file=view.closedFiles().get(e.getKey().value());if(file!=null&&view.open(e.getKey())==null)closed.add(file);} - var provenance=new ExportArtifact.SourceSnapshot(diagrams.serverInstanceId(),success.schema().snapshotId().value(),p.textDocument().getUri(),state.lspVersion,view.workspaceRevision(),revisions); + var provenance=new ExportArtifact.SourceSnapshot(diagrams.serverInstanceId(),success.schema().snapshotId().value(),p.textDocument().getUri(),state.lspVersion(),view.workspaceRevision(),revisions); var artifact=new LspExportService().render(success.schema(),p,provenance,token); - return closed.isEmpty()?CompletableFuture.completedFuture(artifact):verifyFiles.apply(List.copyOf(closed),token).thenApply(ignored->artifact); + return closed.isEmpty()?CompletableFuture.completedFuture(artifact):LspFutures.map(verifyFiles.apply(List.copyOf(closed),token), ignored -> artifact, operations); }); },operations),operations); } @@ -232,7 +231,17 @@ private CompileReport compileReport(guru.interlis.compiler.CompilationOutcome ou return scheduler.onOwner(() -> new LspCompileMapper().map(outcome,diagnosticLanguage,captureView()::uri)); } - private record CompileSelection(List roots,List choices) { } + private void requireSelectedDocument(org.eclipse.lsp4j.VersionedTextDocumentIdentifier document, + long generation, String message) { + var open = documents.synchronizedDocument(document.getUri()); + if (open == null || open.lspVersion() != document.getVersion() + || documents.generation() != generation || documents.needsResync()) { + throw LspFailures.error(ResponseErrorCode.ContentModified, message); + } + } + + private record CompileSelection(List roots, + List choices, long documentGeneration) { } private boolean editorAssist; void editorAssist(boolean enabled){editorAssist=enabled;} @@ -241,145 +250,120 @@ CompletableFuture editorAssist(EditorAssistParams params){ if(params==null||params.protocolVersion()!=1||params.textDocument()==null||params.textDocument().getVersion()==null||params.position()==null||params.options()==null)return failed(ResponseErrorCode.InvalidParams,"editorAssist requires an exact version, position and indentation options"); var p=copyParams(params,EditorAssistParams.class); return requestReady((view,token)->{var state=requireState(view,p.position(),p.textDocument()); - if(state.lspVersion!=p.textDocument().getVersion())return failed(ResponseErrorCode.ContentModified,"editor assistance document version changed"); - if(!editable(view,state.sourceId))return failed(ResponseErrorCode.RequestFailed,"readonly source cannot be edited"); - var plan=new guru.interlis.compiler.query.EditorAssistanceService().planBlockClose(state.analysis,cursor(state,p.position()),new guru.interlis.compiler.query.EditorIndentOptions(p.options().getTabSize(),p.options().isInsertSpaces()),token); - if(plan.isEmpty())return CompletableFuture.completedFuture(null);var value=plan.orElseThrow();String text=state.source.text(); + if(state.lspVersion()!=p.textDocument().getVersion())return failed(ResponseErrorCode.ContentModified,"editor assistance document version changed"); + if(!editable(view,state.sourceId()))return failed(ResponseErrorCode.RequestFailed,"readonly source cannot be edited"); + var plan=new guru.interlis.compiler.query.EditorAssistanceService().planBlockClose(state.analysis(),cursor(state,p.position()),new guru.interlis.compiler.query.EditorIndentOptions(p.options().getTabSize(),p.options().isInsertSpaces()),token); + if(plan.isEmpty())return CompletableFuture.completedFuture(null);var value=plan.orElseThrow();String text=state.source().text(); for(var edit:value.edits().stream().sorted(java.util.Comparator.comparingInt((guru.interlis.source.TextEdit e)->e.range().startOffset()).reversed()).toList())text=text.substring(0,edit.range().startOffset())+edit.replacement()+text.substring(edit.range().endOffset()); - var resultSource=new SourceText(state.sourceId,state.source.revision(),text);var caret=resultSource.lineMap().positionAt(value.caretOffset()); - return CompletableFuture.completedFuture(new EditorAssistResult(1,diagrams.serverInstanceId(),state.analysis.snapshotId().value(),p.textDocument().getUri(),state.lspVersion,value.edits().stream().map(e->new TextEdit(toLspRange(state.source,e.range()),e.replacement())).toList(),new Position(caret.line(),caret.character()))); + var resultSource=new SourceText(state.sourceId(),state.source().revision(),text);var caret=resultSource.lineMap().positionAt(value.caretOffset()); + return CompletableFuture.completedFuture(new EditorAssistResult(1,diagrams.serverInstanceId(),state.analysis().snapshotId().value(),p.textDocument().getUri(),state.lspVersion(),value.edits().stream().map(e->new TextEdit(toLspRange(state.source(),e.range()),e.replacement())).toList(),new Position(caret.line(),caret.character()))); }); } private CompletableFuture requestReady(java.util.function.BiFunction> action) { try { requestGuard.run(); } catch(ResponseErrorException invalid) { return CompletableFuture.failedFuture(invalid); } - return afterFiles(() -> unsynchronized.isEmpty()?scheduler.requestNow(token -> { - HostView view=captureView(); - return scheduler.detached(token,() -> action.apply(view,token)); - }):failed(ResponseErrorCode.ContentModified,"full document resynchronization is required")); + return afterFiles(() -> { + if (documents.needsResync()) return failed(ResponseErrorCode.ContentModified, "full document resynchronization is required"); + long generation = documents.generation(); + return scheduler.requestNow(token -> { + requireInputGeneration(generation); + HostView view = captureView(); + return scheduler.detached(token, () -> action.apply(view, token)); + }); + }); } - private record HostView(Map documents,Map sources, - Map closedFiles,java.util.Set unsynchronized, - boolean versionedEdits, Map externalUris,String workspaceRevision) { + private void requireInputGeneration(long generation) { + if (documents.generation() != generation) { + throw LspFailures.error(ResponseErrorCode.ContentModified, "CONTENT_MODIFIED: workspace inputs changed"); + } + } + + private record HostView(Map documents,Map sources, + Map closedFiles, boolean versionedEdits, + Map externalUris,String workspaceRevision,long documentGeneration) { Optional source(SourceId id) { return Optional.ofNullable(sources.get(id)); } - DocumentState open(SourceId id) { return documents.values().stream().filter(state -> state.sourceId.equals(id)).findFirst().orElse(null); } - String uri(SourceId id) { if(externalUris.containsKey(id)) return externalUris.get(id); return documents.entrySet().stream().filter(entry -> entry.getValue().sourceId.equals(id)).map(Map.Entry::getKey).findFirst() + Synchronized open(SourceId id) { return documents.values().stream().filter(state -> state.sourceId().equals(id)).findFirst().orElse(null); } + String uri(SourceId id) { if(externalUris.containsKey(id)) return externalUris.get(id); return documents.entrySet().stream().filter(entry -> entry.getValue().sourceId().equals(id)).map(Map.Entry::getKey).findFirst() .orElseGet(() -> closedFiles.containsKey(id.value())?closedFiles.get(id.value()).path().toUri().toString():id.value()); } } + private Map captureWorkspaceSources(WorkspaceState state) { + var sources = new LinkedHashMap(); + state.sourceRevisions().forEach((id, revision) -> workspace.source(id) + .filter(source -> source.revision().equals(revision)) + .ifPresent(source -> sources.put(id, source))); + return sources; + } + private HostView captureView() { - var sources=new LinkedHashMap(); - for(var id:workspace.state().sourceRevisions().keySet()) workspace.source(id).ifPresent(source -> sources.put(id,source)); - return new HostView(Map.copyOf(documents),Map.copyOf(sources),Map.copyOf(closedFiles),java.util.Set.copyOf(unsynchronized),versionedEdits,externalSources.uris(),Long.toString(workspace.currentRevision().value())); + var state = workspace.state(); + return captureView(state, captureWorkspaceSources(state), documents.synchronizedDocuments()); + } + + private HostView captureView(WorkspaceState state, Map sources, + Map open) { + return new HostView(open,Map.copyOf(sources),fileIdentities.installedClosedFiles(),versionedEdits, + externalSources.uris(),Long.toString(state.revision().value()),documents.generation()); } private CompletableFuture afterFiles(java.util.function.Supplier> start) { - if(waitingRequests.incrementAndGet()>128) { - waitingRequests.decrementAndGet(); return failed(ResponseErrorCode.RequestFailed,"language request queue is full"); + try { + return scheduler.onOwner(() -> readiness.awaitReady(start)); + } catch (RuntimeException failure) { + return CompletableFuture.failedFuture(LspFailures.response(failure, operations)); } - var active=new java.util.concurrent.atomic.AtomicReference>(); - var result=new CompletableFuture() { - @Override public boolean cancel(boolean interrupt) { - boolean changed=super.cancel(interrupt); var running=active.get(); - if(changed && running!=null) running.cancel(interrupt); return changed; - } - }; - fileWaiters.add(result); - result.whenComplete((ignored,failure) -> { waitingRequests.decrementAndGet(); fileWaiters.remove(result); }); - try { scheduler.onOwner(() -> { orderedDocumentOperation(() -> { - long captured=documentGeneration; - filesReady.whenComplete((ignored,failure) -> { - if(result.isDone()) return; - try { scheduler.onOwner(() -> { - if(captured!=documentGeneration) result.completeExceptionally(LspFailures.error(ResponseErrorCode.ContentModified,"CONTENT_MODIFIED: workspace inputs changed")); - else if(failure!=null) result.completeExceptionally(LspFailures.response(failure,operations)); - else { - var current=start.get(); active.set(current); - if(result.isCancelled()) current.cancel(false); - current.whenComplete((value,problem) -> { - if(problem!=null) result.completeExceptionally(LspFailures.response(problem,operations)); - else result.complete(value); - }); - } - return null; - }); } catch(RuntimeException closed) { result.completeExceptionally(LspFailures.response(closed,operations)); } - }); - }); return null; - }); } catch(RuntimeException failure) { result.completeExceptionally(LspFailures.response(failure,operations)); } - return result; } - boolean isOpen(String uri) { return scheduler.onOwner(() -> openSource(new SourceId(canonicalUriNow(uri)))); } - String canonicalUri(String uri) { return scheduler.onOwner(() -> canonicalUriNow(uri)); } - Map fileSources() { - return scheduler.onOwner(() -> { - var result=new LinkedHashMap(); - closedFiles.forEach((uri,file) -> result.put(new SourceId(uri),file.path().toUri().toString())); - documents.forEach((uri,state) -> result.put(state.sourceId,uri)); rejectedOpens.forEach((uri,state) -> result.put(state.sourceId,uri)); return Map.copyOf(result); - }); + private Map fileSources() { + var result = new LinkedHashMap(); + fileIdentities.addClosedSourceUris(result); + documents.forEachClientSource(result::put); + return Map.copyOf(result); } - Map fileContents() { return scheduler.onOwner(() -> Map.copyOf(closedFiles)); } - List fileAliases(String uri) { + + record FileReload(String canonicalUri, Map knownSources, + Map closedFiles, List aliases) { } + + FileReload beginFileReload(String requestedUri, long request, java.util.function.Predicate register) { + if (requestedUri == null) return null; + var address = LspFileIdentityRegistry.path(requestedUri); + if (address.isEmpty()) return null; return scheduler.onOwner(() -> { - String canonical=canonicalUriNow(uri); - var paths=new java.util.LinkedHashSet(); - var primary=closedFiles.get(canonical); - if(primary!=null) paths.add(primary.path().toUri().toString()); - fileIdentities.forEach((path,id) -> { if(id.equals(canonical)) paths.add(path.toUri().toString()); }); - return List.copyOf(paths); + if (documents.anyClientSource((id, client) -> LspFileIdentityRegistry.path(client).equals(address))) return null; + String canonical = canonicalUriNow(requestedUri); + if (!register.test(canonical)) return null; + var capture = new FileReload(canonical, fileSources(), fileIdentities.installedClosedFiles(), + fileIdentities.aliasesOf(canonical)); + beginExternal(canonical, request); + return capture; }); } - private boolean openSource(SourceId id) { return documents.values().stream().anyMatch(state -> state.sourceId.equals(id)) || rejectedOpens.values().stream().anyMatch(state -> state.sourceId.equals(id)); } - private static Optional filePath(String uri) { - try { var value=java.net.URI.create(uri); return "file".equalsIgnoreCase(value.getScheme()) - ? Optional.of(java.nio.file.Path.of(value).toAbsolutePath().normalize()) : Optional.empty(); } - catch(IllegalArgumentException invalid) { return Optional.empty(); } - } + + private boolean openSource(SourceId id) { return documents.hasClientAuthority(id); } + private String canonicalUriNow(String uri) { - return externalSources.lookup(uri).map(e -> e.source().id().value()).orElseGet(() -> filePath(uri).map(path -> fileIdentities.getOrDefault(path,uri)).orElse(uri)); - } - private String rememberFile(ProjectSourceLoader.FileContent file,Optional openAlias) { - String uri=openAlias.map(SourceId::value).orElseGet(() -> file.fileKey().map(fileKeys::get).orElseGet(() -> - fileIdentities.getOrDefault(file.realPath(),canonicalUriNow(file.path().toUri().toString())))); - int added=(fileIdentities.containsKey(file.path())?0:1) - +(file.path().equals(file.realPath()) || fileIdentities.containsKey(file.realPath())?0:1); - if(fileIdentities.size()+added>40_000) - throw LspFailures.error(ResponseErrorCode.RequestFailed,"file identity limit reached"); - fileIdentities.put(file.path(),uri); fileIdentities.put(file.realPath(),uri); - fileKeys.values().removeIf(uri::equals); - file.fileKey().ifPresent(key -> fileKeys.put(key,uri)); return uri; + var open = documents.find(uri); + if (open != null && open.hasClientAuthority()) return open.sourceId().value(); + return externalSources.lookup(uri).map(e -> e.source().id().value()) + .orElseGet(() -> fileIdentities.canonicalUri(uri)); } - void beginExternal(String uri,long request) { + void beginExternal(String uri, long request) { scheduler.onOwner(() -> { - documentGeneration++; - if(pendingFiles.isEmpty()) { filesReady=new CompletableFuture<>(); filesFailure=null; } - pendingFiles.put(uri,request); scheduler.invalidate(); return null; + readiness.beginExternal(uri, request); + invalidateInputs(); + return null; }); } - private void finishExternal(String uri,long request,Throwable failure) { - if(!Objects.equals(pendingFiles.get(uri),request)) return; - if(failure!=null && filesFailure==null) filesFailure=failure; - pendingFiles.remove(uri); - if(pendingFiles.isEmpty()) { - reanalyzeOpenNow(); - var completed=filesReady; - filesReady=CompletableFuture.completedFuture(null); - if(filesFailure==null) completed.complete(null); else completed.completeExceptionally(LspFailures.response(filesFailure,operations)); - filesFailure=null; - } + private void invalidateInputs() { + documents.advanceGeneration(); + scheduler.invalidate(); } - void externalFailed(String uri,long request,Throwable failure) { - scheduler.onOwner(() -> { - String canonical=canonicalUriNow(uri); - if(Objects.equals(pendingFiles.get(uri),request) && !openSource(new SourceId(canonical))) { - closedFiles.remove(canonical); deleteExternalNow(canonical); - } - finishExternal(uri,request,failure); return null; - }); + void externalFailed(String uri, long request, Throwable failure) { + scheduler.completeOnOwner(() -> readiness.completeExternal(uri, request, failure, this::reanalyzeOpenNow)); } void fileServices(java.util.function.Consumer reload, @@ -391,140 +375,250 @@ void fileServices(java.util.function.Consumer reload, void versionedEdits(boolean supported) { scheduler.onOwner(() -> { versionedEdits=supported; return null; }); } - void installProject(ProjectSourceLoader.Project project,java.util.function.Predicate current) { + void installProject(ProjectSourceLoader.Project project, java.util.function.Predicate current) { scheduler.onOwner(() -> { - var updates=new ArrayList(); - var acceptedFiles=new LinkedHashMap(); - for(var file:project.files()) { - var source=file.loaded(); if(!current.test(source.sourceId().value())) continue; - String primary=project.primaryPaths().get(file.path()).toUri().toString(); - String uri=rememberFile(file,Optional.of(new SourceId(canonicalUriNow(primary)))); var id=new SourceId(uri); - if(acceptedFiles.putIfAbsent(uri,file)!=null || openSource(id)) continue; - var previous=workspace.source(id); - updates.add(new SourceUpdate(id,previous.map(value -> new SourceExpectation.At(value.revision())) - .orElseGet(SourceExpectation.Absent::new),new SourceChange.Replace(source.text()))); + var admitted = project.files().stream().filter(file -> current.test(file.loaded().sourceId().value()) + && current.test(project.primaryPaths().get(file.path()).toUri().toString())).toList(); + fileIdentities.checkCapacity(admitted.stream().map(ProjectSourceLoader.FileContent::identity).toList()); + var updates = new ArrayList(); + var primaries = new LinkedHashMap(); + var aliases = new LinkedHashMap(); + var acceptedFiles = new LinkedHashMap(); + for (var file : admitted) { + var primary = project.primaryPaths().get(file.path()); + String uri = primaries.computeIfAbsent(primary, address -> fileIdentities.sourceForKey(file.identity()) + .orElseGet(() -> canonicalUriNow(address.toUri().toString()))); + if (!current.test(uri) || wouldRebindOpenFile(file.identity(), uri)) continue; + aliases.put(file.identity(), uri); + var id = new SourceId(uri); + if (acceptedFiles.putIfAbsent(uri, file) != null || openSource(id)) continue; + var previous = workspace.source(id); + updates.add(new SourceUpdate(id, previous.map(value -> new SourceExpectation.At(value.revision())) + .orElseGet(SourceExpectation.Absent::new), new SourceChange.Replace(file.loaded().text()))); } - workspace.applyBatch(updates,workspace.currentRevision()); - closedFiles.putAll(acceptedFiles); reanalyzeOpenNow(); return null; + workspace.applyBatch(updates, workspace.currentRevision()); + aliases.forEach(fileIdentities::rememberPhysicalIdentity); + acceptedFiles.forEach(fileIdentities::installClosed); + reanalyzeOpenNow(); + return null; }); } - void applyExternal(String uri,long request,Optional file,Optional openAlias,java.util.function.BooleanSupplier current) { + void applyExternal(String uri, String requestedUri, long request, Optional file, + Optional verifiedSource, boolean requestedMissing, java.util.function.Predicate claimCurrent) { scheduler.onOwner(() -> { - if(!current.getAsBoolean()) return null; - String canonical=file.map(value -> rememberFile(value,openAlias)).orElseGet(() -> canonicalUriNow(uri)); - if(openSource(new SourceId(canonical))) { finishExternal(uri,request,null); return null; } - if(file.isPresent()) { - closedFiles.put(canonical,file.orElseThrow()); - } else closedFiles.remove(canonical); - if(file.isEmpty()) deleteExternalNow(canonical); - else { - var id=new SourceId(canonical); var previous=workspace.source(id); - workspace.update(new SourceUpdate(id,previous.map(value -> new SourceExpectation.At(value.revision())) - .orElseGet(SourceExpectation.Absent::new),new SourceChange.Replace(file.get().loaded().text()))); + if (!readiness.isCurrentExternal(uri, request)) return null; + // Closing the last client can leave only the catalog's physical aliases. + var established = file.flatMap(value -> verifiedSource.map(SourceId::value) + .or(() -> fileIdentities.sourceForKey(value.identity())) + .or(() -> requestedMissing && fileIdentities.isRegisteredKeylessAlias(value.identity(), uri) + ? Optional.of(uri) : Optional.empty())); + String canonical = file.map(value -> established.orElseGet(() -> value.path().toUri().toString())) + .orElseGet(() -> canonicalUriNow(requestedUri)); + if (!claimCurrent.test(canonical)) { + readiness.completeExternal(uri, request, null, this::reanalyzeOpenNow); + return null; + } + var id = new SourceId(canonical); + if (file.isPresent()) { + var loaded = file.orElseThrow(); + if (requestedMissing && !uri.equals(canonical)) { + throw LspFailures.error(ResponseErrorCode.ContentModified, "surviving alias belongs to a different source"); + } + if (wouldRebindOpenFile(loaded.identity(), canonical)) { + readiness.completeExternal(uri, request, null, this::reanalyzeOpenNow); + return null; + } + var existing = fileIdentities.closedFile(canonical); + if (established.isEmpty() && existing != null && !loaded.path().equals(existing.path())) { + throw LspFailures.error(ResponseErrorCode.ContentModified, "file identity changed before refresh"); + } + fileIdentities.checkCapacity(List.of(loaded.identity())); + boolean open = openSource(id); + if (!open) replaceSource(id, loaded.loaded().text()); + if (requestedMissing) forgetMissingFileAlias(requestedUri); + fileIdentities.rememberPhysicalIdentity(loaded.identity(), canonical); + if (!open) { + fileIdentities.installClosed(canonical, loaded); + var publication = publishedClosed.get(id); + if (publication != null && (!publication.uri().equals(loaded.path().toUri().toString()) + || workspace.source(id).filter(source -> source.revision().equals(publication.revision())).isEmpty())) { + clearClosedDiagnostics(id); + } + } + } else { + if (openSource(id)) { + forgetMissingFileAlias(requestedUri); + } else { + workspace.source(id).ifPresent(source -> workspace.remove(id, source.revision())); + fileIdentities.removeClosed(canonical); + clearClosedDiagnostics(id); + } } - finishExternal(uri,request,null); return null; + readiness.completeExternal(uri, request, null, this::reanalyzeOpenNow); + return null; }); } + private boolean wouldRebindOpenFile(ProjectSourceLoader.FileIdentity identity, String canonical) { + return documents.anyClientSource((id, uri) -> !id.value().equals(canonical) + && LspFileIdentityRegistry.path(uri).filter(path -> + path.equals(identity.path()) || path.equals(identity.realPath())).isPresent()); + } + + private void forgetMissingFileAlias(String uri) { + var address = LspFileIdentityRegistry.path(uri); + if (address.isPresent() && !documents.anyClientSource((id, client) -> LspFileIdentityRegistry.path(client).equals(address))) { + fileIdentities.forgetPath(uri); + } + } + void operation(String message) { try { operations.accept(message); } catch(RuntimeException unavailable) { /* disconnected client */ } } @Override public void didOpen(DidOpenTextDocumentParams params) { if(!notificationReady()) return; - var captured=copyParams(params,DidOpenTextDocumentParams.class); - scheduler.onOwner(() -> { orderedDocumentOperation(() -> openWithIdentity(captured)); return null; }); - } - - /** Keeps document notifications and request barriers ordered while bounded IO resolves an unknown URI. */ - private void orderedDocumentOperation(Runnable operation) { - if(!resolvingOpen) { operation.run(); return; } - if(documentOperations.size()>=256) throw LspFailures.error(ResponseErrorCode.RequestFailed,"document operation queue is full"); - documentOperations.addLast(operation); - } - - private void openWithIdentity(DidOpenTextDocumentParams params) { - openWithIdentity(params,0); - } - - private void openWithIdentity(DidOpenTextDocumentParams params,int retries) { - String uri=params.getTextDocument().getUri(); - var known=fileSources(); - if(filePath(uri).isEmpty() || !canonicalUriNow(uri).equals(uri) || known.isEmpty()) { didOpenNow(params); return; } - resolvingOpen=true; long request=++nextOpen; String pending="open-identity:"+request; - beginExternal(pending,request); - identifyFile.apply(uri,known).whenComplete((match,failure) -> { - try { scheduler.onOwner(() -> { - if(failure==null && !known.equals(fileSources())) { - // A watcher may have admitted a hardlink while IO ran, even - // on providers without file keys. Re-resolve that new set. - var changed=retries<3?null:LspFailures.error(ResponseErrorCode.ContentModified,"file identities kept changing; reopen required"); - finishExternal(pending,request,changed); resolvingOpen=false; - if(retries<3) { openWithIdentity(params,retries+1); return null; } - unsynchronized.add(uri); operation("Document identities kept changing; reopen required"); - while(!resolvingOpen && !documentOperations.isEmpty()) documentOperations.removeFirst().run(); - return null; - } - Throwable problem=failure; - try { - if(problem==null) { - var identity=match.identity(); - String canonical=identity.flatMap(value -> value.fileKey().map(fileKeys::get) - .or(() -> Optional.ofNullable(fileIdentities.get(value.realPath())))) - .or(() -> match.source().map(SourceId::value)).orElse(uri); - identity.ifPresent(value -> { - int added=(fileIdentities.containsKey(value.path())?0:1) - +(value.path().equals(value.realPath()) || fileIdentities.containsKey(value.realPath())?0:1); - if(fileIdentities.size()+added>40_000) - throw LspFailures.error(ResponseErrorCode.RequestFailed,"file identity limit reached"); - fileIdentities.put(value.path(),canonical); fileIdentities.put(value.realPath(),canonical); - value.fileKey().ifPresent(key -> fileKeys.put(key,canonical)); - }); - didOpenNow(params); - } else operation("Document identity lookup failed; reopen required"); - } catch(RuntimeException rejected) { problem=rejected; operation("Document identity admission failed; reopen required"); } - finally { finishExternal(pending,request,problem); resolvingOpen=false; } - while(!resolvingOpen && !documentOperations.isEmpty()) documentOperations.removeFirst().run(); - return null; - }); } catch(RuntimeException closed) { operation("Document identity operation stopped"); } + var item=copyParams(params,DidOpenTextDocumentParams.class).getTextDocument(); + scheduler.onOwner(() -> { + readiness.orderDocumentOperation(() -> openWithIdentity(item.getUri(), item.getVersion(), item.getText())); + return null; }); } - private void didOpenNow(DidOpenTextDocumentParams params) { - documentGeneration++; - String uri=params.getTextDocument().getUri(); - SourceId id=new SourceId(canonicalUriNow(uri)); - if(documents.entrySet().stream().anyMatch(entry -> !entry.getKey().equals(uri) && entry.getValue().sourceId.equals(id) && !unsynchronized.contains(entry.getKey()))) { - operation("Document is already open under another URI"); return; + private record OpenInput(String uri, int version, String text) { } + + private void openWithIdentity(String uri, int version, String text) { + var previous = documents.find(uri); + if (previous != null) { + if (version <= previous.lspVersion()) return; + if (!previous.hasClientAuthority()) documents.observeRejectedVersion(uri, version); } - if(uri.startsWith("interlis-source:") && externalSources.lookup(uri).isEmpty()) { - operation("Unknown or expired readonly source handle; navigate again."); return; + var known = fileSources(); + if (LspFileIdentityRegistry.path(uri).isEmpty() || known.isEmpty()) { + didOpenNow(uri, version, text); + } else { + resolveOpenIdentity(new OpenInput(uri, version, text), known, 0, readiness.beginOpen()); } - if(externalSources.contains(id)) { - var source=workspace.source(id).orElseThrow(); - if(externalSources.lookup(uri).filter(entry -> !entry.source().revision().equals(source.revision())).isPresent() || !source.text().equals(params.getTextDocument().getText())) { operation("Readonly source content differs; reopen through definition.");return; } - analyze(uri,params.getTextDocument().getVersion()); return; + } + + private void resolveOpenIdentity(OpenInput input, Map known, int retries, long request) { + long capturedIdentities = fileIdentities.identityRevision(); + try { + invalidateInputs(); + var lookup = Objects.requireNonNull(identifyFile.apply(input.uri(), known)); + readiness.trackOpen(request, lookup); + lookup.whenComplete((match, failure) -> scheduler.completeOnOwner(() -> + completeOpenIdentity(input, known, capturedIdentities, retries, request, match, failure))); + } catch (RuntimeException failure) { + completeOpenIdentity(input, known, capturedIdentities, retries, request, null, failure); } - try { replaceSource(id,params.getTextDocument().getText()); } - catch(guru.interlis.workspace.UpdateRejectedException rejected) { - rejectDocument(uri,id,params.getTextDocument().getVersion(),rejected.code().name()); return; + } + + private void completeOpenIdentity(OpenInput input, Map known, long capturedIdentities, + int retries, long request, ProjectSourceLoader.IdentityMatch match, Throwable failure) { + if (!readiness.isCurrentOpen(request)) return; + Throwable problem = failure; + try { + if (failure != null) { + operation("Document identity lookup failed; reopen required"); + return; + } + var current = fileSources(); + if (capturedIdentities != fileIdentities.identityRevision() || !known.equals(current)) { + // A watcher can admit a new physical alias while the lookup runs. + if (retries >= 3) { + var previous = documents.find(input.uri()); + var id = previous == null ? new SourceId(input.uri()) : previous.sourceId(); + if (!documents.hasSynchronizedAlias(input.uri(), id)) { + documents.advanceGeneration(); + documents.requireResync(input.uri(), id, input.version()).ifPresent(this::clearDocumentDiagnostics); + } + operation("Document identities kept changing; reopen required"); + } else if (current.isEmpty()) { + didOpenNow(input.uri(), input.version(), input.text()); + } else { + resolveOpenIdentity(input, current, retries + 1, readiness.retryOpen(request)); + } + return; + } + match.identity().ifPresent(identity -> { + var verifiedSource = fileIdentities.sourceForKey(identity).or(() -> match.source().map(SourceId::value)); + String canonical = verifiedSource.orElse(input.uri()); + if (wouldRebindOpenFile(identity, canonical)) { + throw LspFailures.error(ResponseErrorCode.ContentModified, "file identity changed before open"); + } + var existing = fileIdentities.closedFile(canonical); + if (verifiedSource.isEmpty() && existing != null && !identity.path().equals(existing.path())) { + throw LspFailures.error(ResponseErrorCode.ContentModified, "file identity changed before open"); + } + fileIdentities.rememberPhysicalIdentity(identity, canonical); + }); + didOpenNow(input.uri(), input.version(), input.text()); + } catch (RuntimeException rejected) { + problem = rejected; + operation("Document identity admission failed; reopen required"); + } finally { + readiness.completeOpen(request, problem, this::reanalyzeOpenNow); } - resynchronized(id,uri); - analyze(uri,params.getTextDocument().getVersion()); } - private void resynchronized(SourceId id,String acceptedUri) { - var oldUris=new java.util.LinkedHashSet(); - rejectedOpens.forEach((uri,state) -> { if(state.sourceId.equals(id)) oldUris.add(uri); }); - documents.forEach((uri,state) -> { if(state.sourceId.equals(id) && unsynchronized.contains(uri)) oldUris.add(uri); }); - for(String uri:oldUris) { - rejectedOpens.remove(uri);unsynchronized.remove(uri); - if(!uri.equals(acceptedUri)) { + private void didOpenNow(String uri, int version, String text) { + documents.advanceGeneration(); + SourceId id = new SourceId(canonicalUriNow(uri)); + if (documents.hasSynchronizedAlias(uri, id)) { + if (documents.find(uri) instanceof LspDocumentRegistry.ResyncRequired resync && !resync.hasClientAuthority()) { documents.remove(uri); - var current=sink.get();if(current!=null) current.publish(new PublishDiagnosticsParams(uri,List.of())); + reanalyzeOpenNow(); + clearDocumentDiagnostics(uri); + } + operation("Document is already open under another URI"); + return; + } + if (uri.startsWith("interlis-source:") && externalSources.currentUri(id).filter(uri::equals).isEmpty()) { + operation("Unknown or expired readonly source handle; navigate again."); + return; + } + if (externalSources.contains(id)) { + var source = workspace.source(id); + if (source.isEmpty()) { + operation("Readonly source is unavailable for analysis."); + return; + } + var installed = source.orElseThrow(); + if (!installed.text().equals(text) || externalSources.lookup(uri) + .filter(entry -> !entry.source().revision().equals(installed.revision())).isPresent()) { + operation("Readonly source content differs; reopen through definition."); + return; } + analyze(uri, id, version); + return; } - unsynchronized.remove(acceptedUri); + try { + replaceSource(id, text); + } catch (guru.interlis.workspace.UpdateRejectedException rejected) { + rejectDocument(uri, id, version, rejected.code().name()); + return; + } + analyze(uri, id, version); + } + + private void clearDocumentDiagnostics(String uri) { + var current = sink.get(); + if (current != null) publish(current, new PublishDiagnosticsParams(uri, List.of())); + } + + private void clearClosedDiagnostics(SourceId id) { + var publication = publishedClosed.remove(id); + if (publication != null) clearDocumentDiagnostics(publication.uri()); + } + + private void retireClosedDiagnostics(Set retired) { + publishedClosed.values().forEach(publication -> retired.add(publication.uri())); + publishedClosed.clear(); + } + + private static void publish(DiagnosticSink sink, PublishDiagnosticsParams params) { + try { sink.publish(params); } catch (RuntimeException disconnected) { /* disconnected client */ } } private void replaceSource(SourceId id,String text) { @@ -533,11 +627,16 @@ private void replaceSource(SourceId id,String text) { .orElseGet(SourceExpectation.Absent::new),new SourceChange.Replace(text))); } - private void rejectDocument(String uri,SourceId id,int version,String code) { - var previous=documents.get(uri); - if(previous!=null) documents.put(uri,new DocumentState(id,version,previous.source,null)); - else rejectedOpens.put(uri,new RejectedOpen(id,version)); - unsynchronized.add(uri); scheduler.invalidate(); + private void rejectDocument(String uri, SourceId id, int version, String code) { + boolean wasOpen = documents.synchronizedDocument(uri) != null; + var retiredAlias = documents.reject(uri, id, version); + reportRejectedDocument(code); + if (wasOpen) clearDocumentDiagnostics(uri); + retiredAlias.ifPresent(this::clearDocumentDiagnostics); + } + + private void reportRejectedDocument(String code) { + scheduler.invalidate(); operation("Document update rejected: "+code+"; full document resynchronization is required (full replacement or reopen)."); } @@ -545,69 +644,85 @@ private void rejectDocument(String uri,SourceId id,int version,String code) { public void didChange(DidChangeTextDocumentParams params) { if(!notificationReady()) return; var captured=copyParams(params,DidChangeTextDocumentParams.class); - scheduler.onOwner(() -> { orderedDocumentOperation(() -> didChangeNow(captured)); return null; }); + scheduler.onOwner(() -> { readiness.orderDocumentOperation(() -> didChangeNow(captured)); return null; }); } private void didChangeNow(DidChangeTextDocumentParams params) { - String uri=params.getTextDocument().getUri(); - var previous=documents.get(uri); var rejected=rejectedOpens.get(uri); - if(externalSources.lookup(uri).isPresent() || (previous!=null && externalSources.contains(previous.sourceId))) { - operation("Readonly repository sources cannot be changed.");return; + String uri = params.getTextDocument().getUri(); + var previous = documents.find(uri); + if (externalSources.lookup(uri).isPresent() || (previous != null && externalSources.contains(previous.sourceId()))) { + operation("Readonly repository sources cannot be changed."); + return; } - int version=params.getTextDocument().getVersion(); - if(previous==null && rejected==null) return; - if(version<=(previous!=null?previous.lspVersion:rejected.version)) return; - documentGeneration++; - SourceId id=previous!=null?previous.sourceId:rejected.sourceId; - var changes=params.getContentChanges(); - if(unsynchronized.contains(uri) && (changes==null || changes.isEmpty() || changes.getFirst()==null || changes.getFirst().getRange()!=null)) { - if(previous!=null) documents.put(uri,new DocumentState(id,version,previous.source,null)); - else rejectedOpens.put(uri,new RejectedOpen(id,version)); + int version = params.getTextDocument().getVersion(); + if (previous == null || version <= previous.lspVersion()) return; + documents.advanceGeneration(); + SourceId id = previous.sourceId(); + var changes = params.getContentChanges(); + if (!(previous instanceof Synchronized) + && (changes == null || changes.isEmpty() || changes.getFirst() == null || changes.getFirst().getRange() != null)) { + documents.observeRejectedVersion(uri, version); return; } - // Only used to interpret a full replacement followed by deltas when an - // oversized initial open had no accepted Core source. Never published. - var base=workspace.source(id).orElseGet(() -> new SourceText(id,guru.interlis.source.SourceRevision.initial(),"")); + // A full replacement supplies its own text before any following deltas. + var base = workspace.source(id).orElseGet(() -> new SourceText(id, guru.interlis.source.SourceRevision.initial(), "")); String text; - try { text=materialize(base,changes); } - catch(ResponseErrorException invalid) { rejectDocument(uri,id,version,"INVALID_EDIT"); return; } - try { replaceSource(id,text); } - catch(guru.interlis.workspace.UpdateRejectedException invalid) { rejectDocument(uri,id,version,invalid.code().name()); return; } - resynchronized(id,uri); analyze(uri,version); + try { + text = materialize(base, changes); + } catch (ResponseErrorException invalid) { + if (previous.hasClientAuthority()) rejectDocument(uri, id, version, "INVALID_EDIT"); + else { + documents.observeRejectedVersion(uri, version); + reportRejectedDocument("INVALID_EDIT"); + } + return; + } + if (!previous.hasClientAuthority()) { + openWithIdentity(uri, version, text); + return; + } + try { + replaceSource(id, text); + } catch (guru.interlis.workspace.UpdateRejectedException invalid) { + rejectDocument(uri, id, version, invalid.code().name()); + return; + } + analyze(uri, id, version); } @Override public void didClose(DidCloseTextDocumentParams params) { if(!notificationReady()) return; var captured=copyParams(params,DidCloseTextDocumentParams.class); - scheduler.onOwner(() -> { orderedDocumentOperation(() -> didCloseNow(captured)); return null; }); + scheduler.onOwner(() -> { readiness.orderDocumentOperation(() -> didCloseNow(captured)); return null; }); } private void didCloseNow(DidCloseTextDocumentParams params) { String uri = params.getTextDocument().getUri(); - var closed=documents.remove(uri); var rejected=rejectedOpens.remove(uri); if(closed==null && rejected==null) return; - documentGeneration++; - unsynchronized.remove(uri); - SourceId id = closed!=null?closed.sourceId:rejected.sourceId; - if(externalSources.contains(id)) { /* close releases editor authority, never repository provenance */ } - else if (filePath(uri).isPresent()) { - reloadExternal(id.value()); - } else { - workspace.source(id).ifPresent(text -> workspace.remove(id, text.revision())); - } - reanalyzeOpen(); - DiagnosticSink current = sink.get(); - if (current != null) { - current.publish(new PublishDiagnosticsParams(params.getTextDocument().getUri(), - List.of())); + var closed = documents.find(uri); + if (closed == null) return; + SourceId id = closed.sourceId(); + boolean local = !externalSources.contains(id); + boolean reload = local && closed.hasClientAuthority() && LspFileIdentityRegistry.path(uri).isPresent(); + if (local && !reload) { + closed.acceptedSource().ifPresent(accepted -> workspace.source(id) + .filter(current -> current.revision().equals(accepted.revision())) + .ifPresent(current -> workspace.remove(id, current.revision()))); } + documents.remove(uri); + documents.advanceGeneration(); + var publication = publishedClosed.get(id); + if (publication != null && publication.uri().equals(uri)) publishedClosed.remove(id); + if (reload) reloadExternal(id.value()); + reanalyzeOpenNow(); + clearDocumentDiagnostics(uri); } @Override public void didSave(DidSaveTextDocumentParams params) { if(!notificationReady()) return; var captured=copyParams(params,DidSaveTextDocumentParams.class); - scheduler.onOwner(() -> { orderedDocumentOperation(() -> didSaveNow(captured)); return null; }); + scheduler.onOwner(() -> { readiness.orderDocumentOperation(() -> didSaveNow(captured)); return null; }); } boolean notificationReady() { @@ -617,10 +732,8 @@ boolean notificationReady() { private void didSaveNow(DidSaveTextDocumentParams params) { String uri = params.getTextDocument().getUri(); - DocumentState state = documents.get(uri); - if (state != null) { - analyze(uri, state.lspVersion); - } + var state = documents.synchronizedDocument(uri); + if (state != null) analyze(uri, state.sourceId(), state.lspVersion()); } @Override @@ -630,9 +743,9 @@ public CompletableFuture hover(HoverParams params) { } private CompletableFuture hoverNow(HostView view, HoverParams params, CancellationToken token) { - DocumentState state = requireState(view, params.getPosition(), params.getTextDocument()); + Synchronized state = requireState(view, params.getPosition(), params.getTextDocument()); SourceRange cursor = cursor(state, params.getPosition()); - QueryResult result = queries.hover(state.analysis, cursor, + QueryResult result = queries.hover(state.analysis(), cursor, token); if (result instanceof QueryResult.Known known) { HoverInfo info = known.value(); @@ -651,9 +764,9 @@ public CompletableFuture, List, List>> definitionNow(HostView view, DefinitionParams params, CancellationToken token) { - DocumentState state = requireState(view, params.getPosition(), params.getTextDocument()); + Synchronized state = requireState(view, params.getPosition(), params.getTextDocument()); SourceRange cursor = cursor(state, params.getPosition()); - QueryResult result = queries.definition(state.analysis, cursor, + QueryResult result = queries.definition(state.analysis(), cursor, token); if (result instanceof QueryResult.Known known) { DefinitionTarget target = known.value(); @@ -674,11 +787,11 @@ public CompletableFuture, CompletionList>> completio } private CompletableFuture, CompletionList>> completionNow(HostView view, CompletionParams params, CancellationToken token) { - DocumentState state = requireState(view, params.getPosition(), params.getTextDocument()); - SourceText source = view.source(state.sourceId).orElseThrow(); + Synchronized state = requireState(view, params.getPosition(), params.getTextDocument()); + SourceText source = view.source(state.sourceId()).orElseThrow(); int offset = offsetAt(source, params.getPosition()); String prefix = prefixAt(source.text(), offset); - CompletionResult result = new guru.interlis.compiler.query.EditorAssistanceService().complete(state.analysis, + CompletionResult result = new guru.interlis.compiler.query.EditorAssistanceService().complete(state.analysis(), new CompletionQuery(cursor(state, params.getPosition()), new NamePrefix(prefix), 100), token); List items = new ArrayList<>(); @@ -689,10 +802,10 @@ private CompletableFuture, CompletionList>> completi item.setDetail(candidate.detail()); item.setInsertText(candidate.insertText()); item.setInsertTextFormat(candidate.snippet()?org.eclipse.lsp4j.InsertTextFormat.Snippet:org.eclipse.lsp4j.InsertTextFormat.PlainText); - item.setTextEdit(Either.forLeft(new TextEdit(toLspRange(state.source, result.replacementRange()), candidate.insertText()))); + item.setTextEdit(Either.forLeft(new TextEdit(toLspRange(state.source(), result.replacementRange()), candidate.insertText()))); if(candidate.snippet()) { String template=candidate.label().replace(" block","");int stops=switch(template){case "MODEL"->4;case "CLASS","STRUCTURE","TOPIC"->2;case "DOMAIN","UNIT"->2;case "VIEW TOPIC"->3;default->0;}; - item.setCommand(new Command("INTERLIS snippet interaction","interlisGuru.snippet.accept",List.of(Map.of("uri",params.getTextDocument().getUri(),"baseVersion",state.lspVersion,"headerStops",stops,"suggestAtBody",java.util.Set.of("DOMAIN","UNIT").contains(template),"suppressHeader",template.equals("MODEL"))))); + item.setCommand(new Command("INTERLIS snippet interaction","interlisGuru.snippet.accept",List.of(Map.of("uri",params.getTextDocument().getUri(),"baseVersion",state.lspVersion(),"headerStops",stops,"suggestAtBody",java.util.Set.of("DOMAIN","UNIT").contains(template),"suppressHeader",template.equals("MODEL"))))); } item.setSortText(String.format(java.util.Locale.ROOT, "%06d", rank++)); items.add(item); @@ -708,22 +821,22 @@ public CompletableFuture>> documentSymbolNow(HostView view, DocumentSymbolParams params, CancellationToken token) { String uri = params.getTextDocument().getUri(); - DocumentState state = requireState(view, new Position(0, 0), params.getTextDocument()); + Synchronized state = requireState(view, new Position(0, 0), params.getTextDocument()); List> result = new ArrayList<>(); var byId=new LinkedHashMap(); - for (Symbol symbol : state.analysis.semanticGraph().declarationsInOrder()) { + for (Symbol symbol : state.analysis().semanticGraph().declarationsInOrder()) { if (symbol.origin() instanceof guru.interlis.model.Origin.DeclaredOrigin declared - && declared.range().source().equals(state.sourceId) + && declared.range().source().equals(state.sourceId()) && symbol.nameRange().isPresent()) { DocumentSymbol documentSymbol = new DocumentSymbol(symbol.name().value(), symbolKind(symbol), - toLspRange(state.source, declared.range()), - toLspRange(state.source, symbol.nameRange().get())); + toLspRange(state.source(), declared.range()), + toLspRange(state.source(), symbol.nameRange().get())); documentSymbol.setChildren(new ArrayList<>()); byId.put(symbol.id(),documentSymbol); } } - for(Symbol symbol:state.analysis.semanticGraph().declarationsInOrder()) { + for(Symbol symbol:state.analysis().semanticGraph().declarationsInOrder()) { var item=byId.get(symbol.id()); if(item==null) continue; var parent=symbol.owner().map(byId::get).orElse(null); if(parent==null) result.add(Either.forRight(item)); else parent.getChildren().add(item); @@ -738,16 +851,16 @@ public CompletableFuture> references(ReferenceParams pa } private CompletableFuture> referencesNow(HostView view, ReferenceParams params, CancellationToken token) { - DocumentState state = requireState(view, params.getPosition(), params.getTextDocument()); + Synchronized state = requireState(view, params.getPosition(), params.getTextDocument()); SourceRange cursor = cursor(state, params.getPosition()); - QueryResult selected = queries.symbolAt(state.analysis, cursor, + QueryResult selected = queries.symbolAt(state.analysis(), cursor, token); if (!(selected instanceof QueryResult.Known known)) { return CompletableFuture.completedFuture(List.of()); } boolean includeDeclaration = params.getContext() == null || params.getContext().isIncludeDeclaration(); - ReferenceResult result = queries.references(state.analysis, known.value(), + ReferenceResult result = queries.references(state.analysis(), known.value(), new ReferenceOptions(includeDeclaration), token); if(!result.complete()) return failed(ResponseErrorCode.RequestFailed,"workspace reference search is incomplete"); List locations = new ArrayList<>(); @@ -769,8 +882,8 @@ PrepareRenameDefaultBehavior>> prepareRename(PrepareRenameParams params) { private CompletableFuture> prepareRenameNow(HostView view, PrepareRenameParams params, CancellationToken token) { - DocumentState state = requireState(view, params.getPosition(), params.getTextDocument()); - RenamePreparation preparation = queries.prepareRename(state.analysis, + Synchronized state = requireState(view, params.getPosition(), params.getTextDocument()); + RenamePreparation preparation = queries.prepareRename(state.analysis(), cursor(state, params.getPosition()), token); if (!preparation.allowed()) { return failed(ResponseErrorCode.RequestFailed, @@ -793,17 +906,17 @@ public CompletableFuture rename(RenameParams params) { } private CompletableFuture renameNow(HostView view, RenameParams params, CancellationToken token) { - DocumentState state = requireState(view, params.getPosition(), params.getTextDocument()); + Synchronized state = requireState(view, params.getPosition(), params.getTextDocument()); if(params.getNewName()==null) return failed(ResponseErrorCode.InvalidParams,"newName is required"); - QueryResult selected = queries.symbolAt(state.analysis, + QueryResult selected = queries.symbolAt(state.analysis(), cursor(state, params.getPosition()), token); if (!(selected instanceof QueryResult.Known known)) { return failed(ResponseErrorCode.RequestFailed, "no editable symbol at cursor"); } RenamePlan plan; try { - plan = queries.planRename(state.analysis, new RenameRequest(known.value(), - params.getNewName(), state.analysis.sourceRevisions()), + plan = queries.planRename(state.analysis(), new RenameRequest(known.value(), + params.getNewName(), state.analysis().sourceRevisions()), token); } catch (IllegalArgumentException invalidName) { return failed(ResponseErrorCode.InvalidParams, invalidName.getMessage()); @@ -833,13 +946,13 @@ private CompletableFuture renameNow(HostView view, RenameParams p String uri=view.uri(document.source()); changes.put(uri, edits); var open=view.open(document.source()); versioned.add(Either.forLeft(new TextDocumentEdit(new org.eclipse.lsp4j.VersionedTextDocumentIdentifier( - uri,open==null?null:open.lspVersion),edits))); + uri,open==null?null:open.lspVersion()),edits))); if(open==null) closed.add(view.closedFiles().get(document.source().value())); } WorkspaceEdit edit = new WorkspaceEdit(); if(view.versionedEdits()) { edit.setChanges(null); edit.setDocumentChanges(versioned); } else edit.setChanges(changes); return closed.isEmpty() ? CompletableFuture.completedFuture(edit) - : verifyFiles.apply(List.copyOf(closed),token).thenApply(ignored -> edit); + : LspFutures.map(verifyFiles.apply(List.copyOf(closed),token), ignored -> edit, operations); } private static boolean editable(HostView view,SourceId id) { @@ -855,20 +968,20 @@ public CompletableFuture> formatting(DocumentFormatting } private CompletableFuture> formattingNow(HostView view, DocumentFormattingParams params, CancellationToken token) { - DocumentState state = requireState(view, new Position(0, 0), params.getTextDocument()); - if(!editable(view,state.sourceId)) return failed(ResponseErrorCode.RequestFailed,"readonly source cannot be formatted"); + Synchronized state = requireState(view, new Position(0, 0), params.getTextDocument()); + if(!editable(view,state.sourceId())) return failed(ResponseErrorCode.RequestFailed,"readonly source cannot be formatted"); FormatOptions options = LspFormattingOptions.decode(params.getOptions()); - FormatOutcome outcome = ModelFormatters.create().format(state.source, options, token); + FormatOutcome outcome = ModelFormatters.create().format(state.source(), options, token); if(outcome instanceof FormatOutcome.Cancelled cancelled) return failed(ResponseErrorCode.RequestCancelled,"formatting cancelled: "+cancelled.reason()); if(outcome instanceof FormatOutcome.Rejected) return failed(ResponseErrorCode.RequestFailed,"source cannot be formatted without changing its meaning"); var formatted = (FormatOutcome.Formatted) outcome; formatted.notices().stream().map(notice -> notice.code() + ": " + notice.message()) .distinct().limit(3).forEach(this::operation); - if (formatted.text().equals(state.source.text())) { + if (formatted.text().equals(state.source().text())) { return CompletableFuture.completedFuture(List.of()); } return CompletableFuture.completedFuture(List.of(new TextEdit( - toLspRange(state.source, state.source.range(0, state.source.length())), + toLspRange(state.source(), state.source().range(0, state.source().length())), formatted.text()))); } @@ -879,13 +992,13 @@ public CompletableFuture>> codeAction(CodeActio } private CompletableFuture>> codeActionNow(HostView view, CodeActionParams params, CancellationToken token) { - DocumentState state = requireState(view, params.getRange().getStart(), params.getTextDocument()); - if(!editable(view,state.sourceId)) return CompletableFuture.completedFuture(List.of()); + Synchronized state = requireState(view, params.getRange().getStart(), params.getTextDocument()); + if(!editable(view,state.sourceId())) return CompletableFuture.completedFuture(List.of()); var result=new ArrayList>(); - if(view.versionedEdits())for(var plan:new guru.interlis.compiler.query.EditorAssistanceService().quickFixes(state.analysis,state.source.range(offsetAt(state.source,params.getRange().getStart()),offsetAt(state.source,params.getRange().getEnd())),token)) { + if(view.versionedEdits())for(var plan:new guru.interlis.compiler.query.EditorAssistanceService().quickFixes(state.analysis(),state.source().range(offsetAt(state.source(),params.getRange().getStart()),offsetAt(state.source(),params.getRange().getEnd())),token)) { var action=new CodeAction(plan.title());action.setKind("quickfix");action.setIsPreferred(true); - var edits=plan.edits().stream().map(e->new TextEdit(toLspRange(state.source,e.range()),e.replacement())).toList(); - var edit=new WorkspaceEdit();edit.setChanges(null);edit.setDocumentChanges(List.of(Either.forLeft(new TextDocumentEdit(new org.eclipse.lsp4j.VersionedTextDocumentIdentifier(view.uri(state.sourceId),state.lspVersion),edits))));action.setEdit(edit);result.add(Either.forRight(action)); + var edits=plan.edits().stream().map(e->new TextEdit(toLspRange(state.source(),e.range()),e.replacement())).toList(); + var edit=new WorkspaceEdit();edit.setChanges(null);edit.setDocumentChanges(List.of(Either.forLeft(new TextDocumentEdit(new org.eclipse.lsp4j.VersionedTextDocumentIdentifier(view.uri(state.sourceId()),state.lspVersion()),edits))));action.setEdit(edit);result.add(Either.forRight(action)); } var only=params.getContext()==null?null:params.getContext().getOnly(); if(only!=null&&!only.isEmpty())return CompletableFuture.completedFuture(result.stream().filter(action->action.isRight()&&only.stream().anyMatch(kind->kind.equals(action.getRight().getKind())||action.getRight().getKind().startsWith(kind+"."))).toList()); @@ -895,9 +1008,9 @@ private CompletableFuture>> codeActionNow(HostV @Override public CompletableFuture> onTypeFormatting(org.eclipse.lsp4j.DocumentOnTypeFormattingParams params) { var p=copyParams(params,org.eclipse.lsp4j.DocumentOnTypeFormattingParams.class); if(editorAssist||!"\n".equals(p.getCh()))return CompletableFuture.completedFuture(List.of()); - return requestReady((view,token)->{var state=requireState(view,p.getPosition(),p.getTextDocument());if(!editable(view,state.sourceId))return CompletableFuture.completedFuture(List.of()); - var plan=new guru.interlis.compiler.query.EditorAssistanceService().planBlockClose(state.analysis,cursor(state,p.getPosition()),new guru.interlis.compiler.query.EditorIndentOptions(p.getOptions().getTabSize(),p.getOptions().isInsertSpaces()),token); - return CompletableFuture.completedFuture(plan.map(value->value.edits().stream().map(e->new TextEdit(toLspRange(state.source,e.range()),e.replacement())).toList()).orElse(List.of()));}); + return requestReady((view,token)->{var state=requireState(view,p.getPosition(),p.getTextDocument());if(!editable(view,state.sourceId()))return CompletableFuture.completedFuture(List.of()); + var plan=new guru.interlis.compiler.query.EditorAssistanceService().planBlockClose(state.analysis(),cursor(state,p.getPosition()),new guru.interlis.compiler.query.EditorIndentOptions(p.getOptions().getTabSize(),p.getOptions().isInsertSpaces()),token); + return CompletableFuture.completedFuture(plan.map(value->value.edits().stream().map(e->new TextEdit(toLspRange(state.source(),e.range()),e.replacement())).toList()).orElse(List.of()));}); } void configurationChanged(Object settings) { @@ -912,14 +1025,13 @@ void configurationChanged(Object settings) { boolean repositoryChanged=!host.options().equals(repositorySettings.options()) || !host.cache().equals(repositorySettings.cache()); boolean compilerChanged=!configuration.equals(userConfiguration); var current=workspace.configuration(); + var expiredPublications = new LinkedHashSet(); if(repositoryChanged) { // An expired virtual editor must not reserve its old SourceId as // an active open alias when fresh navigation opens a new handle. - for(var entry:List.copyOf(documents.entrySet())) if(externalSources.contains(entry.getValue().sourceId)) { - documents.remove(entry.getKey());unsynchronized.remove(entry.getKey());rejectedOpens.remove(entry.getKey()); - var publisher=sink.get();if(publisher!=null) publisher.publish(new PublishDiagnosticsParams(entry.getKey(),List.of())); - } for(var id:externalSources.uris().keySet()) workspace.source(id).ifPresent(source -> workspace.remove(id,source.revision())); + expiredPublications.addAll(documents.removeSources(externalSources::contains)); + retireClosedDiagnostics(expiredPublications); externalSources.close(); current=configuration; } else if(compilerChanged) { @@ -930,7 +1042,8 @@ void configurationChanged(Object settings) { } var receipt=workspace.configure(current,workspace.state().revision()); userConfiguration=configuration; applyHostSettings(host); - if(receipt.changed() || hostChanged) { documentGeneration++; reanalyzeOpenNow(); } + if(receipt.changed() || hostChanged) { documents.advanceGeneration(); reanalyzeOpenNow(); } + expiredPublications.forEach(this::clearDocumentDiagnostics); return null; }); } @@ -949,36 +1062,18 @@ void initializeConfiguration(Object settings) { void reloadExternal(String uri) { Objects.requireNonNull(externalReload,"project file service").accept(uri); } - void deleteExternal(String uri) { - scheduler.onOwner(() -> { - String canonical=canonicalUriNow(uri); - if(openSource(new SourceId(canonical))) return null; - documentGeneration++; closedFiles.remove(canonical); deleteExternalNow(canonical); - var request=pendingFiles.get(uri); - if(request!=null) finishExternal(uri,request,null); else reanalyzeOpenNow(); - return null; - }); - } - - private void deleteExternalNow(String uri) { - if (openSource(new SourceId(canonicalUriNow(uri)))) return; - SourceId id = new SourceId(canonicalUriNow(uri)); - workspace.source(id).ifPresent(text -> workspace.remove(id, text.revision())); - fileKeys.values().removeIf(id.value()::equals); - } - void reanalyzeOpen() { scheduler.onOwner(() -> { reanalyzeOpenNow(); return null; }); } private void reanalyzeOpenNow() { - for (String uri : List.copyOf(documents.keySet())) { - DocumentState state = documents.get(uri); - if (state != null) { - documents.put(uri,new DocumentState(state.sourceId,state.lspVersion,state.source,null)); - } - } - if(pendingFiles.isEmpty() && unsynchronized.isEmpty()) scheduler.schedule(); else scheduler.invalidate(); + documents.invalidateAnalysis(); + scheduleOpenAnalysis(); + } + + private void scheduleOpenAnalysis() { + if (readiness.isReady() && !documents.needsResync()) scheduler.schedule(); + else scheduler.invalidate(); } private static T copyParams(T value, Class type) { @@ -999,7 +1094,7 @@ public CompletableFuture semanticTokensFull(SemanticTokensParams } private CompletableFuture semanticTokensFullNow(HostView view, SemanticTokensParams params, CancellationToken token) { - DocumentState state = requireState(view, new Position(0, 0), params.getTextDocument()); + Synchronized state = requireState(view, new Position(0, 0), params.getTextDocument()); List data = semanticTokens(state,token); return CompletableFuture.completedFuture(new SemanticTokens(data)); } @@ -1011,24 +1106,24 @@ public CompletableFuture> foldingRange(FoldingRangeRequestPar } private CompletableFuture> foldingRangeNow(HostView view, FoldingRangeRequestParams params, CancellationToken token) { - DocumentState state = requireState(view, new Position(0, 0), params.getTextDocument()); + Synchronized state = requireState(view, new Position(0, 0), params.getTextDocument()); List ranges = new ArrayList<>(); - for (Symbol symbol : state.analysis.semanticGraph().declarationsInOrder()) { + for (Symbol symbol : state.analysis().semanticGraph().declarationsInOrder()) { token.throwIfCancellationRequested(); if (symbol.origin() instanceof guru.interlis.model.Origin.DeclaredOrigin declared - && declared.range().source().equals(state.sourceId)) { - var range = toLspRange(state.source, declared.range()); + && declared.range().source().equals(state.sourceId())) { + var range = toLspRange(state.source(), declared.range()); if (range.getEnd().getLine() > range.getStart().getLine()) { ranges.add(new FoldingRange(range.getStart().getLine(), range.getEnd().getLine())); } } } - state.analysis.syntaxSnapshot(state.sourceId).ifPresent(syntax -> { + state.analysis().syntaxSnapshot(state.sourceId()).ifPresent(syntax -> { for(var trivia:syntax.trivia()) { token.throwIfCancellationRequested(); if(trivia.kind()==guru.interlis.syntax.TriviaKind.WHITESPACE_GAP) continue; - var range=toLspRange(state.source,trivia.range()); + var range=toLspRange(state.source(),trivia.range()); if(range.getEnd().getLine()>range.getStart().getLine()) { var fold=new FoldingRange(range.getStart().getLine(),range.getEnd().getLine()); fold.setKind("comment"); ranges.add(fold); @@ -1042,18 +1137,18 @@ private CompletableFuture> foldingRangeNow(HostView view, Fol static final List TOKEN_TYPES=List.of("keyword","number","string","enumMember","function","variable","property","namespace","class","type","comment"); - private List semanticTokens(DocumentState state,CancellationToken cancellation) { + private List semanticTokens(Synchronized state,CancellationToken cancellation) { record Span(SourceRange range,int type) { } var spans=new ArrayList(); - state.analysis.syntaxSnapshot(state.sourceId).ifPresent(syntax -> { + state.analysis().syntaxSnapshot(state.sourceId()).ifPresent(syntax -> { for(var token:syntax.tokens()) { cancellation.throwIfCancellationRequested(); int type=switch(token.kind()) { case KEYWORD -> 0; case NUMBER -> 1; case STRING -> 2; case IDENTIFIER -> { - var result=queries.symbolAt(state.analysis,state.source.range(token.range().startOffset(),token.range().startOffset()),cancellation); + var result=queries.symbolAt(state.analysis(),state.source().range(token.range().startOffset(),token.range().startOffset()),cancellation); if(result instanceof QueryResult.Known known) { - var symbol=state.analysis.semanticGraph().symbol(known.value().symbol()).orElseThrow(); + var symbol=state.analysis().semanticGraph().symbol(known.value().symbol()).orElseThrow(); yield switch(symbol.kind()) { case MODEL,TOPIC,CONTEXT,METADATA_BASKET -> 7; case CLASS,STRUCTURE,ASSOCIATION,VIEW -> 8; @@ -1074,7 +1169,7 @@ yield switch(symbol.kind()) { List data = new ArrayList<>(); int lastLine = 0; int lastCharacter = 0; - var lines=state.source.lineMap(); + var lines=state.source().lineMap(); for(var span:spans) { int first=lines.positionAt(span.range().startOffset()).line(); int last=lines.positionAt(span.range().endOffset()).line(); @@ -1097,15 +1192,15 @@ CompletableFuture diagram(DiagramParams params) { } private CompletableFuture diagramNow(HostView view, DiagramParams params, CancellationToken token) { - DocumentState state = requireState(view, new Position(0, 0), new TextDocumentIdentifier(params.uri())); + Synchronized state = requireState(view, new Position(0, 0), new TextDocumentIdentifier(params.uri())); if (params.snapshotId() != null - && !params.snapshotId().equals(state.analysis.snapshotId().value())) { + && !params.snapshotId().equals(state.analysis().snapshotId().value())) { return CompletableFuture.failedFuture(new org.eclipse.lsp4j.jsonrpc.ResponseErrorException( new org.eclipse.lsp4j.jsonrpc.messages.ResponseError( org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode.InvalidParams, "snapshotId is stale for the requested document", null))); } - var request=new DiagramRequest(1,"legacy-command",new org.eclipse.lsp4j.VersionedTextDocumentIdentifier(params.uri(),state.lspVersion),params.symbolIds(),null); + var request=new DiagramRequest(1,"legacy-command",new org.eclipse.lsp4j.VersionedTextDocumentIdentifier(params.uri(),state.lspVersion()),params.symbolIds(),null); var response=diagrams.project(request,diagramContext(view,state,params.uri()),token); return CompletableFuture.completedFuture(new DiagramJsonCodec().encode(response.document())); } @@ -1115,7 +1210,7 @@ CompletableFuture diagramRequest(DiagramRequest params) { var captured=copyParams(params,DiagramRequest.class); return requestReady((view,token)-> { var state=requireState(view,new Position(0,0),captured.textDocument()); - if(state.lspVersion!=captured.textDocument().getVersion())return failed(ResponseErrorCode.ContentModified,"diagram document version changed"); + if(state.lspVersion()!=captured.textDocument().getVersion())return failed(ResponseErrorCode.ContentModified,"diagram document version changed"); return CompletableFuture.completedFuture(diagrams.project(captured,diagramContext(view,state,captured.textDocument().getUri()),token)); }); } @@ -1125,13 +1220,13 @@ CompletableFuture diagramNavigate(DiagramNavigationParams par var captured=copyParams(params,DiagramNavigationParams.class); return requestReady((view,token)-> { var state=requireState(view,new Position(0,0),captured.textDocument()); - if(state.lspVersion!=captured.textDocument().getVersion())return failed(ResponseErrorCode.ContentModified,"diagram document version changed"); + if(state.lspVersion()!=captured.textDocument().getVersion())return failed(ResponseErrorCode.ContentModified,"diagram document version changed"); var anchor=diagrams.navigation(captured,diagramContext(view,state,captured.textDocument().getUri())); var range=anchor.nameRange();var source=view.source(range.source()).orElse(null); if(source==null||!source.revision().equals(range.revision()))return failed(ResponseErrorCode.ContentModified,"diagram source revision changed"); var result=new DiagramNavigation(diagrams.serverInstanceId(),anchor.snapshotId().value(),view.uri(range.source()),toLspRange(source,range),!editable(view,range.source())); var closed=view.closedFiles().get(range.source().value()); - return closed!=null&&view.open(range.source())==null?verifyFiles.apply(List.of(closed),token).thenApply(ignored->result):CompletableFuture.completedFuture(result); + return closed!=null&&view.open(range.source())==null?LspFutures.map(verifyFiles.apply(List.of(closed),token), ignored -> result, operations):CompletableFuture.completedFuture(result); }); } @@ -1143,9 +1238,9 @@ CompletableFuture diagramSelect(DiagramSelectionParams p return requestReady((view,token)-> { var root=requireState(view,new Position(0,0),captured.textDocument()); var source=requireState(view,captured.position(),captured.sourceDocument()); - if(root.lspVersion!=captured.textDocument().getVersion()||source.lspVersion!=captured.sourceDocument().getVersion()) + if(root.lspVersion()!=captured.textDocument().getVersion()||source.lspVersion()!=captured.sourceDocument().getVersion()) return failed(ResponseErrorCode.ContentModified,"source selection document version changed"); - var selected=queries.symbolAt(source.analysis,cursor(source,captured.position()),token); + var selected=queries.symbolAt(source.analysis(),cursor(source,captured.position()),token); SymbolRef symbol=selected instanceof QueryResult.Known known?known.value():null; token.throwIfCancellationRequested(); return CompletableFuture.completedFuture(diagrams.selection(captured,diagramContext(view,root,captured.textDocument().getUri()),symbol)); @@ -1153,39 +1248,40 @@ CompletableFuture diagramSelect(DiagramSelectionParams p } private static Map documentVersions(HostView view) { - var result=new LinkedHashMap();view.documents().forEach((uri,state)->result.put(uri,state.lspVersion));return Map.copyOf(result); + var result=new LinkedHashMap();view.documents().forEach((uri,state)->result.put(uri,state.lspVersion()));return Map.copyOf(result); } - private static LspDiagramService.Context diagramContext(HostView view,DocumentState state,String uri) { - return new LspDiagramService.Context(state.analysis,state.sourceId,uri,state.lspVersion,view.workspaceRevision(),documentVersions(view)); + private static LspDiagramService.Context diagramContext(HostView view,Synchronized state,String uri) { + return new LspDiagramService.Context(state.analysis(),state.sourceId(),uri,state.lspVersion(),view.workspaceRevision(),documentVersions(view)); } - private static DocumentState requireState(HostView view,Position position, TextDocumentIdentifier document) { + private static Synchronized requireState(HostView view,Position position, TextDocumentIdentifier document) { if(document==null || document.getUri()==null || document.getUri().isBlank()) throw LspFailures.error(ResponseErrorCode.InvalidParams,"document URI is required"); String uri = document.getUri(); var direct=view.documents().get(uri); - DocumentState state = direct!=null?direct:view.open(new SourceId(uri)); + Synchronized state = direct!=null?direct:view.open(new SourceId(uri)); if (state == null) { throw new ResponseErrorException(new ResponseError(ResponseErrorCode.InvalidParams, "document is not available", null)); } - if(uri.startsWith("interlis-source:") && !uri.equals(view.externalUris().get(state.sourceId))) + if(uri.startsWith("interlis-source:") && !uri.equals(view.externalUris().get(state.sourceId()))) throw LspFailures.error(ResponseErrorCode.ContentModified,"readonly source handle expired; navigate again"); - if (view.unsynchronized().contains(view.uri(state.sourceId))) throw new ResponseErrorException(new ResponseError( - ResponseErrorCode.ContentModified, "full document resynchronization is required", null)); - if (view.source(state.sourceId).filter(source -> source.revision().equals(state.source.revision())).isEmpty() - || state.analysis == null) throw new ResponseErrorException(new ResponseError( + if (view.source(state.sourceId()).filter(source -> source.revision().equals(state.source().revision())).isEmpty() + || state.currentAnalysis().isEmpty()) throw new ResponseErrorException(new ResponseError( ResponseErrorCode.ContentModified, "CONTENT_MODIFIED: document revision changed", null)); - PositionMapper.toOffset(state.source, position); + PositionMapper.toOffset(state.source(), position); return state; } - private void analyze(String uri, int lspVersion) { - SourceId sourceId = new SourceId(canonicalUriNow(uri)); + private void analyze(String uri, SourceId sourceId, int version) { SourceText source = workspace.source(sourceId).orElseThrow(); - DocumentState state = new DocumentState(sourceId, lspVersion, source, null); - documents.put(uri, state); - if(pendingFiles.isEmpty() && unsynchronized.isEmpty()) scheduler.schedule(); else scheduler.invalidate(); + var retiredAlias = documents.synchronize(uri, version, source); + var closed = publishedClosed.get(sourceId); + boolean retireClosed = closed != null && (!closed.uri().equals(uri) || !closed.revision().equals(source.revision())) + && publishedClosed.remove(sourceId, closed); + scheduleOpenAnalysis(); + if (retireClosed) clearDocumentDiagnostics(closed.uri()); + retiredAlias.filter(alias -> !retireClosed || !alias.equals(closed.uri())).ifPresent(this::clearDocumentDiagnostics); } private void applyHostSettings(RepositorySettings settings) { @@ -1197,11 +1293,21 @@ private void applyHostSettings(RepositorySettings settings) { }; } - private void repositoryResolved(guru.interlis.repository.RepositoryResolutionOutcome result) { - repositoryFailure=result instanceof guru.interlis.repository.RepositoryResolutionOutcome.Rejected rejected?rejected:null; - if(result instanceof guru.interlis.repository.RepositoryResolutionOutcome.Prepared prepared) { - for(var update:prepared.update().sources()) workspace.source(update.source()).ifPresent(source -> - externalSources.register(source,workspace.configuration().compilerOptions().budget().maxTotalSourceUtf16Length())); + private void repositoryResolved(RepositoryResolutionOutcome.Prepared prepared) { + var retired = new LinkedHashSet(); + for (var update : prepared.update().sources()) { + workspace.source(update.source()).ifPresent(source -> registerReadonlySource(source, retired)); + } + retired.forEach(this::clearDocumentDiagnostics); + } + + private void registerReadonlySource(SourceText source, Set retired) { + var previous = externalSources.currentUri(source.id()); + externalSources.register(source, workspace.configuration().compilerOptions().budget().maxTotalSourceUtf16Length()); + if (previous.isPresent() && !previous.equals(externalSources.currentUri(source.id()))) { + retired.addAll(documents.removeSources(source.id()::equals)); + var publication = publishedClosed.remove(source.id()); + if (publication != null) retired.add(publication.uri()); } } @@ -1216,78 +1322,110 @@ CompletableFuture sourceContent(SourceContentParams params) { } catch(RuntimeException invalid) { return CompletableFuture.failedFuture(LspFailures.response(invalid,operations)); } } - private void accepted(AnalysisSnapshot analysis) { - if(!unsynchronized.isEmpty()) return; - publishClosedDiagnostics(analysis); - for(var diagnostic:analysis.diagnostics()) if(diagnostic.primaryRange().isEmpty()) - operation(diagnosticMapper.global(diagnostic,diagnosticLanguage)); - for (String uri : List.copyOf(documents.keySet())) { - if (unsynchronized.contains(uri)) continue; - var previous = documents.get(uri); - var source = workspace.source(previous.sourceId); - if (source.isEmpty() || !source.get().revision().equals(previous.source.revision()) - || !source.get().revision().equals(analysis.sourceRevisions().get(previous.sourceId))) continue; - var state = new DocumentState(previous.sourceId, previous.lspVersion, source.orElseThrow(), analysis); - documents.put(uri, state); - publish(uri,state); - } - var view=captureView(); - analysisReady.accept(new AnalysisReady(1,diagrams.serverInstanceId(),view.workspaceRevision(),analysis.snapshotId().value(),documentVersions(view))); - } - - private void publishClosedDiagnostics(AnalysisSnapshot analysis) { - var current=sink.get();if(current==null) return; - var view=captureView();var sources=new LinkedHashMap<>(view.sources()); - var diagnostics=new ArrayList<>(analysis.diagnostics()); - if(repositoryFailure!=null) { - repositoryFailure.sources().forEach((id,source) -> { - sources.putIfAbsent(id,source); - if(!view.sources().containsKey(id)) externalSources.register(source,workspace.configuration().compilerOptions().budget().maxTotalSourceUtf16Length()); + private void accepted(AnalysisSnapshot analysis, Optional rejection) { + if (!readiness.isReady() || documents.needsResync()) return; + var output = sink.get(); + var state = workspace.state(); + var sources = captureWorkspaceSources(state); + if (!state.revision().equals(workspace.currentRevision())) return; + var retired = new LinkedHashSet(); + var diagnostics = new ArrayList<>(analysis.diagnostics()); + rejection.ifPresent(rejected -> { + diagnostics.addAll(rejected.diagnostics()); + if (output == null) return; + rejected.sources().forEach((id, source) -> { + if (!state.sourceRevisions().containsKey(id)) { + registerReadonlySource(source, retired); + sources.put(id, source); + } }); - diagnostics.addAll(repositoryFailure.diagnostics()); + }); + + var open = documents.synchronizedDocuments(); + var openUris = new LinkedHashMap(); + var versionedSources = new LinkedHashSet(); + open.forEach((uri, previous) -> { + SourceId id = previous.sourceId(); + openUris.put(id, uri); + var source = sources.get(id); + if (source != null && !source.revision().equals(previous.source().revision())) { + // This editor cannot display ranges from the newer Workspace text. + sources.remove(id); + } else if (source != null && source.revision().equals(analysis.sourceRevisions().get(id))) { + documents.acceptAnalysis(uri, previous, analysis); + versionedSources.add(id); + } + }); + var view = captureView(state, sources, open); + + retired.forEach(this::clearDocumentDiagnostics); + for (var diagnostic : diagnostics) { + if (!publicationCurrent(view)) return; + if (diagnostic.primaryRange().isEmpty()) operation(diagnosticMapper.global(diagnostic, diagnosticLanguage)); } - var externalUris=externalSources.uris(); - java.util.function.Function uri=id -> externalUris.getOrDefault(id,view.uri(id)); - var byUri=new LinkedHashMap>(); - for(var d:diagnostics) { - if(d.primaryRange().isEmpty()) { if(repositoryFailure!=null && repositoryFailure.diagnostics().contains(d)) operation(diagnosticMapper.global(d,diagnosticLanguage));continue; } - var range=d.primaryRange().orElseThrow(); - if(view.open(range.source())!=null) continue; - var source=sources.get(range.source()); - if(source==null || !source.revision().equals(range.revision())) continue; - byUri.computeIfAbsent(uri.apply(range.source()),ignored -> new ArrayList<>()).add(diagnosticMapper.toLsp(d,sources,diagnosticLanguage,uri)); + if (output != null && publicationCurrent(view)) { + publishDiagnostics(output, view, openUris, versionedSources, diagnostics); + } + if (publicationCurrent(view)) { + analysisReady.accept(new AnalysisReady(1, diagrams.serverInstanceId(), view.workspaceRevision(), + analysis.snapshotId().value(), documentVersions(view))); } - for(String old:publishedExternal) if(!byUri.containsKey(old)) current.publish(new PublishDiagnosticsParams(old,List.of())); - byUri.forEach((address,items) -> current.publish(new PublishDiagnosticsParams(address,items))); - publishedExternal.clear();publishedExternal.addAll(byUri.keySet()); } - private void publish(String uri,DocumentState state) { - DiagnosticSink current = sink.get(); - if (current == null) { - return; + private void publishDiagnostics(DiagnosticSink output, HostView view, Map openUris, + Set versionedSources, List diagnostics) { + java.util.function.Function address = id -> { + String openUri = openUris.get(id); + return openUri != null ? openUri : view.uri(id); + }; + var bySource = new LinkedHashMap>(); + for (var diagnostic : diagnostics) { + diagnostic.primaryRange().ifPresent(range -> { + var id = range.source(); + if ((!openUris.containsKey(id) || versionedSources.contains(id)) + && view.source(id).filter(source -> source.revision().equals(range.revision())).isPresent()) { + bySource.computeIfAbsent(range.source(), ignored -> new ArrayList<>()) + .add(diagnosticMapper.toLsp(diagnostic, view.sources(), diagnosticLanguage, address)); + } + }); } - List diagnostics = new ArrayList<>(); - var displayed=new ArrayList<>(state.analysis.diagnostics()); - if(repositoryFailure!=null) displayed.addAll(repositoryFailure.diagnostics()); - for (Diagnostic diagnostic : displayed) { - if (diagnostic.primaryRange().isEmpty()) { - continue; - } - SourceRange range = diagnostic.primaryRange().get(); - if (!range.source().equals(state.sourceId)) { - continue; + + for (var id : List.copyOf(publishedClosed.keySet())) { + if (!publicationCurrent(view)) return; + var previous = publishedClosed.get(id); + if (previous == null) continue; + String openUri = openUris.get(id); + if (openUri != null && versionedSources.contains(id) && previous.uri().equals(openUri)) continue; + if (openUri != null || !bySource.containsKey(id) || !previous.uri().equals(address.apply(id))) { + clearClosedDiagnostics(id); } - var view=captureView(); - org.eclipse.lsp4j.Diagnostic lsp=diagnosticMapper.toLsp(diagnostic,view.sources(),diagnosticLanguage,view::uri); - diagnostics.add(lsp); } - diagnostics.sort(Comparator.comparingInt((org.eclipse.lsp4j.Diagnostic d) -> - d.getRange().getStart().getLine()) - .thenComparingInt(d -> d.getRange().getStart().getCharacter())); - var publication = new PublishDiagnosticsParams(uri, diagnostics); - publication.setVersion(state.lspVersion); - current.publish(publication); + for (var entry : bySource.entrySet()) { + if (!publicationCurrent(view)) return; + SourceId id = entry.getKey(); + if (openUris.containsKey(id)) continue; + String uri = address.apply(id); + publishedClosed.put(id, new ClosedPublication(uri, view.source(id).orElseThrow().revision())); + publish(output, new PublishDiagnosticsParams(uri, entry.getValue())); + } + for (var id : versionedSources) { + if (!publicationCurrent(view)) return; + String uri = openUris.get(id); + var items = bySource.getOrDefault(id, List.of()).stream() + .sorted(Comparator.comparingInt((org.eclipse.lsp4j.Diagnostic diagnostic) -> + diagnostic.getRange().getStart().getLine()) + .thenComparingInt(diagnostic -> diagnostic.getRange().getStart().getCharacter())) + .toList(); + var publication = new PublishDiagnosticsParams(uri, items); + publication.setVersion(view.documents().get(uri).lspVersion()); + publishedClosed.remove(id); + publish(output, publication); + } + } + + private boolean publicationCurrent(HostView view) { + return readiness.isReady() && documents.generation() == view.documentGeneration() + && Long.toString(workspace.currentRevision().value()).equals(view.workspaceRevision()); } void locale(String locale) { @@ -1317,9 +1455,9 @@ private static String materialize(SourceText base, List { + String address = LspFileIdentityRegistry.path(requestedUri).orElseThrow().toUri().toString(); + if (fileRequests.getOrDefault(address, 0L) > request) return false; + if (fileRequests.merge(canonical, request, Math::max) != request) return false; + fileRequests.put(address, request); + return true; + }); + } + + /** The result may identify an alias of a different dispatch key. Claim its actual source too. */ + boolean claimFileRequest(String dispatchedUri, String resolvedUri, long request) { + return fileRequests.getOrDefault(dispatchedUri, 0L) == request + && fileRequests.merge(resolvedUri, request, Math::max) == request; + } + private void reloadFile(String requestedUri) { - if(requestedUri==null) return; - String uri=documents.canonicalUri(requestedUri); - if(uri==null || !uri.startsWith("file:") || documents.isOpen(uri)) return; - long request=nextFileRequest.incrementAndGet(); fileRequests.put(uri,request); - var known=documents.fileSources(); - var contents=documents.fileContents(); - var aliases=documents.fileAliases(uri); - documents.beginExternal(uri,request); + long request = nextFileRequest.incrementAndGet(); + var capture = beginFileReload(requestedUri, request); + if (capture == null) return; + String uri = capture.canonicalUri(); + String address = LspFileIdentityRegistry.path(requestedUri).orElseThrow().toUri().toString(); + var capturedRoots = roots; requestIo(token -> { - var loaded=files.read(requestedUri,roots,token); - if(loaded.isEmpty()) for(String alias:aliases) { - loaded=files.read(alias,roots,token); if(loaded.isPresent()) break; + var loaded = files.read(requestedUri, capturedRoots, token); + boolean requestedMissing = loaded.isEmpty(); + var previous = capture.closedFiles().get(uri); + if (requestedMissing) { + for (String alias : capture.aliases()) { + var candidate = files.read(alias, capturedRoots, token); + if (candidate.isPresent() && survivingAddress(previous, candidate.orElseThrow())) { + loaded = candidate; + break; + } + } } - // Prefer the explicit surviving primary for edits. If it was removed, - // retain the Core identity but use the surviving physical address. - if(loaded.isPresent() && !aliases.isEmpty() && files.sameFile(loaded.orElseThrow(),aliases.getFirst(),token)) - loaded=files.read(aliases.getFirst(),roots,token); - token.throwIfCancellationRequested(); - var openAlias=loaded.flatMap(file -> known.entrySet().stream().filter(entry -> files.sameFile(file,entry.getValue(),token)).map(Map.Entry::getKey).findFirst()); - if(openAlias.isEmpty() && loaded.isPresent()) { - var identity=loaded.orElseThrow().fileKey(); - openAlias=identity.flatMap(key -> contents.entrySet().stream().filter(entry -> entry.getValue().fileKey().map(key::equals).orElse(false)) + var known = capture.knownSources(); + var verified = loaded.flatMap(file -> known.entrySet().stream() + .filter(entry -> files.sameFile(file, entry.getValue(), token)).map(Map.Entry::getKey).findFirst()); + if (verified.isEmpty() && loaded.isPresent()) { + var file = loaded.orElseThrow(); + verified = file.fileKey().flatMap(key -> capture.closedFiles().entrySet().stream() + .filter(entry -> entry.getValue().fileKey().map(key::equals).orElse(false)) .map(entry -> new SourceId(entry.getKey())).findFirst()); - // On a keyless provider a registered surviving address still - // carries its established Core identity after primary removal. - if(openAlias.isEmpty() && known.containsKey(new SourceId(uri)) && aliases.contains(requestedUri) - && files.read(known.get(new SourceId(uri)),roots,token).isEmpty()) openAlias=java.util.Optional.of(new SourceId(uri)); + // A provider without a key can retain a previously proved alias after its primary disappears. + var source = new SourceId(uri); + if (verified.isEmpty() && file.fileKey().isEmpty() && known.containsKey(source) + && capture.aliases().contains(file.path().toUri().toString()) + && files.read(known.get(source), capturedRoots, token).isEmpty()) verified = java.util.Optional.of(source); } - if(openAlias.isPresent() && loaded.isPresent()) { - String primary=known.get(openAlias.orElseThrow()); - if(primary!=null && files.sameFile(loaded.orElseThrow(),primary,token)) loaded=files.read(primary,roots,token); + if (verified.isPresent() && loaded.isPresent()) { + String primary = known.get(verified.orElseThrow()); + var observed = loaded.orElseThrow(); + if (primary != null && LspFileIdentityRegistry.path(primary).filter(observed.path()::equals).isEmpty() + && files.sameFile(observed, primary, token)) { + var preferred = files.read(primary, capturedRoots, token); + if (preferred.isPresent() && files.sameFile(observed, primary, token)) loaded = preferred; + } } - if(openAlias.isEmpty()) openAlias=java.util.Optional.of(new SourceId(known.containsKey(new SourceId(requestedUri))?requestedUri: - loaded.isEmpty()?uri:loaded.orElseThrow().path().toUri().toString())); - documents.applyExternal(uri,request,loaded,openAlias,() -> !token.isCancellationRequested() && fileRequests.getOrDefault(uri,0L)==request); + token.throwIfCancellationRequested(); + documents.applyExternal(uri, requestedUri, request, loaded, verified, requestedMissing, + resolved -> !token.isCancellationRequested() && fileRequests.getOrDefault(address, 0L) == request + && claimFileRequest(uri, resolved, request)); return null; }).exceptionally(failure -> { - try { documents.externalFailed(uri,request,failure); } catch(RuntimeException closed) { } + try { documents.externalFailed(uri, request, failure); } catch(RuntimeException closed) { } documents.operation("External source refresh failed; " - +(failure instanceof org.eclipse.lsp4j.jsonrpc.ResponseErrorException expected?expected.getResponseError().getMessage():"internal file error")); return null; + +(failure instanceof org.eclipse.lsp4j.jsonrpc.ResponseErrorException expected + ?expected.getResponseError().getMessage():"internal file error")); + return null; }); } + private static boolean survivingAddress(ProjectSourceLoader.FileContent previous, ProjectSourceLoader.FileContent candidate) { + return previous == null || previous.path().equals(candidate.path()) + || previous.fileKey().isEmpty() || candidate.fileKey().isEmpty() + || previous.fileKey().equals(candidate.fileKey()); + } + @Override public void didChangeConfiguration(DidChangeConfigurationParams params) { if(!documents.notificationReady()) return; @@ -153,10 +188,7 @@ public void didChangeConfiguration(DidChangeConfigurationParams params) { @Override public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { if(!documents.notificationReady()) return; - for (var event : params.getChanges()) { - if(documents.isOpen(event.getUri())) continue; - reloadFile(event.getUri()); - } + for (var event : params.getChanges()) reloadFile(event.getUri()); } @Override diff --git a/interlis-lsp/src/main/java/guru/interlis/lsp/LspDocumentRegistry.java b/interlis-lsp/src/main/java/guru/interlis/lsp/LspDocumentRegistry.java new file mode 100644 index 00000000..04a7e5b2 --- /dev/null +++ b/interlis-lsp/src/main/java/guru/interlis/lsp/LspDocumentRegistry.java @@ -0,0 +1,198 @@ +package guru.interlis.lsp; + +import guru.interlis.compiler.AnalysisSnapshot; +import guru.interlis.source.SourceId; +import guru.interlis.source.SourceText; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +/** Owner-queue document state. Entries are immutable and keyed by the exact client URI. */ +final class LspDocumentRegistry { + private final Map entries = new LinkedHashMap<>(); + private long generation; + + sealed interface Entry permits Synchronized, ResyncRequired, RejectedOpen { + SourceId sourceId(); + int lspVersion(); + + /** Last accepted client text, if available. Resync text is never a query input. */ + default Optional acceptedSource() { + return switch (this) { + case Synchronized open -> Optional.of(open.source()); + case ResyncRequired resync -> resync.lastAccepted(); + case RejectedOpen ignored -> Optional.empty(); + }; + } + + /** A failed identity lookup alone does not give the client authority over a physical file. */ + default boolean hasClientAuthority() { + return !(this instanceof ResyncRequired resync && resync.lastAccepted().isEmpty()); + } + } + + record Synchronized(SourceId sourceId, int lspVersion, SourceText source, + Optional currentAnalysis) implements Entry { + Synchronized { + Objects.requireNonNull(sourceId, "sourceId"); + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(currentAnalysis, "currentAnalysis"); + if (!sourceId.equals(source.id()) || currentAnalysis.isPresent() + && !source.revision().equals(currentAnalysis.orElseThrow().sourceRevisions().get(sourceId))) { + throw new IllegalArgumentException("document source and analysis revisions differ"); + } + } + + /** Returns the current analysis after the host has checked that it is available. */ + AnalysisSnapshot analysis() { + return currentAnalysis.orElseThrow(); + } + } + + record ResyncRequired(SourceId sourceId, int lspVersion, + Optional lastAccepted) implements Entry { + ResyncRequired { + Objects.requireNonNull(sourceId, "sourceId"); + Objects.requireNonNull(lastAccepted, "lastAccepted"); + if (lastAccepted.isPresent() && !sourceId.equals(lastAccepted.orElseThrow().id())) { + throw new IllegalArgumentException("resync text belongs to another source"); + } + } + } + + record RejectedOpen(SourceId sourceId, int lspVersion) implements Entry { + RejectedOpen { + Objects.requireNonNull(sourceId, "sourceId"); + } + } + + long generation() { + return generation; + } + + void advanceGeneration() { + generation++; + } + + /** Returns the exact client's state or null when it is closed. */ + Entry find(String uri) { + return entries.get(uri); + } + + /** Returns the exact client's synchronized state or null. */ + Synchronized synchronizedDocument(String uri) { + return entries.get(uri) instanceof Synchronized open ? open : null; + } + + boolean needsResync() { + return entries.values().stream().anyMatch(entry -> !(entry instanceof Synchronized)); + } + + boolean hasSynchronizedAlias(String uri, SourceId sourceId) { + return entries.entrySet().stream().anyMatch(entry -> !entry.getKey().equals(uri) + && entry.getValue() instanceof Synchronized open && open.sourceId().equals(sourceId)); + } + + boolean hasClientAuthority(SourceId sourceId) { + return entries.values().stream().anyMatch(entry -> entry.hasClientAuthority() && entry.sourceId().equals(sourceId)); + } + + boolean anyClientSource(BiPredicate matches) { + return entries.entrySet().stream().anyMatch(entry -> entry.getValue().hasClientAuthority() + && matches.test(entry.getValue().sourceId(), entry.getKey())); + } + + void forEachClientSource(BiConsumer consumer) { + entries.forEach((uri, entry) -> { + if (entry.hasClientAuthority()) consumer.accept(entry.sourceId(), uri); + }); + } + + /** Immutable snapshot in client admission order, containing only synchronized buffers. */ + Map synchronizedDocuments() { + Map snapshot = new LinkedHashMap<>(); + entries.forEach((uri, entry) -> { + if (entry instanceof Synchronized open) snapshot.put(uri, open); + }); + return Collections.unmodifiableMap(snapshot); + } + + Optional synchronize(String uri, int version, SourceText source) { + return replace(uri, new Synchronized(source.id(), version, source, Optional.empty())); + } + + Optional reject(String uri, SourceId sourceId, int version) { + var accepted = acceptedSource(sourceId); + Entry rejected = accepted.isPresent() + ? new ResyncRequired(sourceId, version, accepted) + : new RejectedOpen(sourceId, version); + return replace(uri, rejected); + } + + Optional requireResync(String uri, SourceId sourceId, int version) { + return replace(uri, new ResyncRequired(sourceId, version, acceptedSource(sourceId))); + } + + void observeRejectedVersion(String uri, int version) { + Entry updated = switch (entries.get(uri)) { + case ResyncRequired resync -> new ResyncRequired(resync.sourceId(), version, resync.lastAccepted()); + case RejectedOpen rejected -> new RejectedOpen(rejected.sourceId(), version); + default -> throw new IllegalStateException("document does not require resynchronization"); + }; + entries.put(uri, updated); + } + + void invalidateAnalysis() { + entries.replaceAll((uri, entry) -> entry instanceof Synchronized open && open.currentAnalysis().isPresent() + ? new Synchronized(open.sourceId(), open.lspVersion(), open.source(), Optional.empty()) + : entry); + } + + Synchronized acceptAnalysis(String uri, Synchronized previous, AnalysisSnapshot analysis) { + var current = new Synchronized(previous.sourceId(), previous.lspVersion(), previous.source(), Optional.of(analysis)); + if (!entries.replace(uri, previous, current)) { + throw new IllegalStateException("document changed before accepting analysis"); + } + return current; + } + + void remove(String uri) { + entries.remove(uri); + } + + List removeSources(Predicate predicate) { + var removed = entries.entrySet().stream().filter(entry -> predicate.test(entry.getValue().sourceId())) + .map(Map.Entry::getKey).toList(); + removed.forEach(entries::remove); + return removed; + } + + void clear() { + entries.clear(); + } + + private Optional acceptedSource(SourceId sourceId) { + return entries.values().stream().filter(entry -> entry.sourceId().equals(sourceId)) + .findFirst().flatMap(Entry::acceptedSource); + } + + /** Replaces the client state and returns any superseded unsynchronized alias. */ + private Optional replace(String uri, Entry entry) { + var alias = entries.entrySet().stream().filter(previous -> !previous.getKey().equals(uri) + && previous.getValue().sourceId().equals(entry.sourceId())).findFirst(); + if (alias.isPresent() && alias.orElseThrow().getValue() instanceof Synchronized) { + throw new IllegalStateException("source already has a synchronized client"); + } + var removed = alias.map(Map.Entry::getKey); + removed.ifPresent(entries::remove); + entries.put(uri, entry); + return removed; + } +} diff --git a/interlis-lsp/src/main/java/guru/interlis/lsp/LspFileIdentityRegistry.java b/interlis-lsp/src/main/java/guru/interlis/lsp/LspFileIdentityRegistry.java new file mode 100644 index 00000000..5565e80d --- /dev/null +++ b/interlis-lsp/src/main/java/guru/interlis/lsp/LspFileIdentityRegistry.java @@ -0,0 +1,144 @@ +package guru.interlis.lsp; + +import guru.interlis.lsp.ProjectSourceLoader.FileContent; +import guru.interlis.lsp.ProjectSourceLoader.FileIdentity; +import guru.interlis.source.SourceId; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; + +import java.net.URI; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** Owner-queue catalog of local identities established by the file I/O adapter. */ +final class LspFileIdentityRegistry { + private static final int MAX_ALIAS_PATHS = 40_000; + + private record Alias(String uri, Optional fileKey) { } + + private final Map closedFiles = new LinkedHashMap<>(); + private final Map aliases = new LinkedHashMap<>(); + private final Map keys = new LinkedHashMap<>(); + private long identityRevision; + + /** Normalizes the spelling of a local URI without consulting the filesystem. */ + static Optional path(String uri) { + try { + var value = URI.create(uri); + return "file".equalsIgnoreCase(value.getScheme()) + ? Optional.of(Path.of(value).toAbsolutePath().normalize()) : Optional.empty(); + } catch (IllegalArgumentException invalid) { + return Optional.empty(); + } + } + + String canonicalUri(String uri) { + return path(uri).map(aliases::get).map(Alias::uri).orElse(uri); + } + + long identityRevision() { + return identityRevision; + } + + Optional sourceForKey(FileIdentity identity) { + return identity.fileKey().map(keys::get); + } + + /** A surviving address on a keyless filesystem retains its previously proved association. */ + boolean isRegisteredKeylessAlias(FileIdentity identity, String canonicalUri) { + var alias = aliases.get(identity.path()); + return identity.fileKey().isEmpty() && alias != null && alias.fileKey().isEmpty() + && alias.uri().equals(canonicalUri); + } + + void checkCapacity(List identities) { + var additional = new LinkedHashSet(); + for (var identity : identities) { + if (!aliases.containsKey(identity.path())) additional.add(identity.path()); + if (!aliases.containsKey(identity.realPath())) additional.add(identity.realPath()); + } + checkCapacity(additional.size()); + } + + /** Registers only paths and keys obtained from a verified physical observation. */ + void rememberPhysicalIdentity(FileIdentity identity, String canonicalUri) { + int added = aliases.containsKey(identity.path()) ? 0 : 1; + if (!identity.path().equals(identity.realPath()) && !aliases.containsKey(identity.realPath())) added++; + checkCapacity(added); + + var previous = aliases.get(identity.path()); + if (previous != null && previous.uri().equals(canonicalUri) + && previous.fileKey().isPresent() && identity.fileKey().isPresent() + && !previous.fileKey().equals(identity.fileKey())) { + // The logical primary was replaced. Its old hardlinks are no longer aliases of the new file. + var retiredKey = previous.fileKey().orElseThrow(); + aliases.values().removeIf(alias -> alias.uri().equals(canonicalUri) && alias.fileKey().equals(previous.fileKey())); + keys.remove(retiredKey, canonicalUri); + identityRevision++; + } + var alias = new Alias(canonicalUri, identity.fileKey()); + boolean changed = !alias.equals(aliases.put(identity.path(), alias)); + changed |= !alias.equals(aliases.put(identity.realPath(), alias)); + if (identity.fileKey().isPresent()) { + changed |= !canonicalUri.equals(keys.put(identity.fileKey().orElseThrow(), canonicalUri)); + } + if (changed) identityRevision++; + } + + void forgetPath(String uri) { + path(uri).ifPresent(address -> { + if (aliases.remove(address) != null) identityRevision++; + }); + } + + void installClosed(String uri, FileContent file) { + closedFiles.put(uri, file); + } + + /** Removes a source only after the host confirms that no physical address survives. */ + void removeClosed(String uri) { + closedFiles.remove(uri); + boolean changed = aliases.values().removeIf(alias -> alias.uri().equals(uri)); + changed |= keys.values().removeIf(uri::equals); + if (changed) identityRevision++; + } + + FileContent closedFile(String uri) { + return closedFiles.get(uri); + } + + /** Immutable captured metadata for query workers and file I/O. */ + Map installedClosedFiles() { + return Map.copyOf(closedFiles); + } + + void addClosedSourceUris(Map sources) { + closedFiles.forEach((uri, file) -> sources.put(new SourceId(uri), file.path().toUri().toString())); + } + + /** The current edit address is tried first, then the other registered physical paths. */ + List aliasesOf(String canonicalUri) { + var paths = new LinkedHashSet(); + var primary = closedFiles.get(canonicalUri); + if (primary != null) paths.add(primary.path().toUri().toString()); + aliases.forEach((path, alias) -> { + if (canonicalUri.equals(alias.uri())) paths.add(path.toUri().toString()); + }); + return List.copyOf(paths); + } + + void clear() { + closedFiles.clear(); + aliases.clear(); + keys.clear(); + } + + private void checkCapacity(int added) { + if (aliases.size() + added > MAX_ALIAS_PATHS) { + throw LspFailures.error(ResponseErrorCode.RequestFailed, "file identity limit reached"); + } + } +} diff --git a/interlis-lsp/src/main/java/guru/interlis/lsp/LspInputReadiness.java b/interlis-lsp/src/main/java/guru/interlis/lsp/LspInputReadiness.java new file mode 100644 index 00000000..69598342 --- /dev/null +++ b/interlis-lsp/src/main/java/guru/interlis/lsp/LspInputReadiness.java @@ -0,0 +1,266 @@ +package guru.interlis.lsp; + +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; + +import java.util.ArrayDeque; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.LongSupplier; +import java.util.function.Supplier; + +/** File admission and request ordering on the existing analysis owner queue. */ +final class LspInputReadiness { + private static final int MAX_REQUESTS = 128; + private static final int MAX_DOCUMENT_OPERATIONS = 256; + + private final LongSupplier documentGeneration; + private final Consumer completeOnOwner; + private final Consumer operations; + private final Map pendingFiles = new LinkedHashMap<>(); + private final Set> requests = new LinkedHashSet<>(); + private final ArrayDeque deferred = new ArrayDeque<>(); + private FileFailure fileFailure; + private OpenLookup open; + private long nextOpen; + private boolean draining; + private boolean closed; + + private record FileFailure(long generation, Throwable cause) { } + + private record OpenLookup(long token, CompletableFuture future) { } + + private record DocumentOperation(Runnable action, Request request) { } + + LspInputReadiness(LongSupplier documentGeneration, Consumer completeOnOwner, + Consumer operations) { + this.documentGeneration = documentGeneration; + this.completeOnOwner = completeOnOwner; + this.operations = operations; + } + + void beginExternal(String uri, long token) { + if (closed) throw shutdownFailure(); + pendingFiles.put(uri, token); + } + + boolean isCurrentExternal(String uri, long token) { + return Objects.equals(pendingFiles.get(uri), token); + } + + void completeExternal(String uri, long token, Throwable failure, Runnable reanalyze) { + if (!pendingFiles.remove(uri, token)) return; + settle(failure, reanalyze); + } + + long beginOpen() { + if (closed) throw shutdownFailure(); + if (open != null) throw new IllegalStateException("document identity is already being resolved"); + return nextOpen(); + } + + long retryOpen(long token) { + if (!isCurrentOpen(token)) throw new IllegalStateException("document identity lookup changed"); + return nextOpen(); + } + + private long nextOpen() { + open = new OpenLookup(++nextOpen, null); + return open.token(); + } + + boolean isCurrentOpen(long token) { + return open != null && open.token() == token; + } + + void trackOpen(long token, CompletableFuture future) { + if (isCurrentOpen(token)) open = new OpenLookup(token, future); + else future.cancel(false); + } + + void completeOpen(long token, Throwable failure, Runnable reanalyze) { + if (!isCurrentOpen(token)) return; + open = null; + try { + settle(failure, reanalyze); + } finally { + drainDocumentOperations(); + } + } + + boolean isReady() { + return !closed && !hasPendingFiles() && currentFailure() == null; + } + + void orderDocumentOperation(Runnable action) { + order(new DocumentOperation(action, null)); + } + + CompletableFuture awaitReady(Supplier> start) { + if (closed) return CompletableFuture.failedFuture(shutdownFailure()); + requests.removeIf(CompletableFuture::isDone); + if (requests.size() >= MAX_REQUESTS) { + return CompletableFuture.failedFuture(LspFailures.error(ResponseErrorCode.RequestFailed, "language request queue is full")); + } + var request = new Request<>(start); + requests.add(request); + request.whenComplete((ignored, failure) -> completeOnOwner.accept(() -> { + requests.remove(request); + deferred.removeIf(operation -> operation.request() == request); + })); + try { + order(new DocumentOperation(() -> awaitFiles(request), request)); + } catch (RuntimeException failure) { + request.fail(failure); + } + return request; + } + + void cancelPendingOnClose() { + closed = true; + var identity = open; + open = null; + pendingFiles.clear(); + fileFailure = null; + deferred.clear(); + var cancelled = List.copyOf(requests); + requests.clear(); + cancelled.forEach(Request::shutdown); + if (identity != null && identity.future() != null) identity.future().cancel(false); + } + + private void order(DocumentOperation operation) { + if (closed) throw shutdownFailure(); + if (open == null && !draining && deferred.isEmpty()) { + operation.action().run(); + return; + } + deferred.removeIf(queued -> queued.request() != null && queued.request().isDone()); + if (deferred.size() >= MAX_DOCUMENT_OPERATIONS) { + throw LspFailures.error(ResponseErrorCode.RequestFailed, "document operation queue is full"); + } + deferred.addLast(operation); + } + + private void drainDocumentOperations() { + if (draining) return; + draining = true; + try { + while (!closed && open == null && !deferred.isEmpty()) { + var operation = deferred.removeFirst(); + var request = operation.request(); + if (request != null && request.isDone()) continue; + try { + operation.action().run(); + } catch (RuntimeException failure) { + if (request != null) request.fail(failure); + else if (failure instanceof ResponseErrorException expected) operations.accept(expected.getResponseError().getMessage()); + else LspFailures.response(failure, operations); + } + } + } finally { + draining = false; + } + } + + private void awaitFiles(Request request) { + if (request.isDone()) return; + request.generation = documentGeneration.getAsLong(); + if (hasPendingFiles()) request.waitingForFiles = true; + else release(request); + } + + private void settle(Throwable failure, Runnable reanalyze) { + if (failure != null && currentFailure() == null) { + fileFailure = new FileFailure(documentGeneration.getAsLong(), failure); + } + if (hasPendingFiles()) return; + var waiting = requests.stream().filter(request -> request.waitingForFiles).toList(); + waiting.forEach(request -> request.waitingForFiles = false); + try { + reanalyze.run(); + } catch (RuntimeException problem) { + if (currentFailure() == null) fileFailure = new FileFailure(documentGeneration.getAsLong(), problem); + } finally { + waiting.forEach(this::release); + } + } + + private boolean hasPendingFiles() { + return open != null || !pendingFiles.isEmpty(); + } + + private Throwable currentFailure() { + return fileFailure != null && fileFailure.generation() == documentGeneration.getAsLong() + ? fileFailure.cause() : null; + } + + private void release(Request request) { + if (request.isDone()) return; + if (request.generation != documentGeneration.getAsLong()) { + request.fail(LspFailures.error(ResponseErrorCode.ContentModified, "CONTENT_MODIFIED: workspace inputs changed")); + } else if (currentFailure() != null) { + request.fail(currentFailure()); + } else { + request.start(); + } + } + + private static ResponseErrorException shutdownFailure() { + return LspFailures.error(ResponseErrorCode.RequestCancelled, "server is closing"); + } + + private final class Request extends CompletableFuture { + private Supplier> start; + private long generation; + private boolean waitingForFiles; + // Only the owner assigns the stage. A client can cancel it from any thread. + private volatile CompletableFuture active; + + Request(Supplier> start) { + this.start = start; + } + + void start() { + var action = start; + start = null; + try { + var stage = Objects.requireNonNull(action.get()); + active = stage; + if (isDone()) stage.cancel(false); + else stage.whenComplete((value, failure) -> { + if (failure == null) complete(value); + else fail(failure); + }); + } catch (RuntimeException failure) { + fail(failure); + } + } + + void fail(Throwable failure) { + completeExceptionally(LspFailures.response(failure, operations)); + } + + void shutdown() { + completeExceptionally(shutdownFailure()); + cancelActive(false); + } + + @Override public boolean cancel(boolean interrupt) { + boolean cancelled = super.cancel(interrupt); + if (cancelled) cancelActive(interrupt); + return cancelled; + } + + private void cancelActive(boolean interrupt) { + var stage = active; + if (stage != null) stage.cancel(interrupt); + } + } +} diff --git a/interlis-lsp/src/main/java/guru/interlis/lsp/ProjectSourceLoader.java b/interlis-lsp/src/main/java/guru/interlis/lsp/ProjectSourceLoader.java index 4590c94a..0c61cd84 100644 --- a/interlis-lsp/src/main/java/guru/interlis/lsp/ProjectSourceLoader.java +++ b/interlis-lsp/src/main/java/guru/interlis/lsp/ProjectSourceLoader.java @@ -16,6 +16,7 @@ final class ProjectSourceLoader { private static final Set EXCLUDED=Set.of(".git",".gradle","build","node_modules"); record FileContent(Path path,Path realPath,Optional fileKey,LoadedSource loaded,boolean editable,List roots,boolean symbolicLink) { FileContent { roots=List.copyOf(roots); } + FileIdentity identity() { return new FileIdentity(path, realPath, fileKey); } } record Project(List roots,List files,Map primaryPaths) { Project { roots=List.copyOf(roots); files=List.copyOf(files); primaryPaths=Map.copyOf(primaryPaths); } @@ -106,6 +107,7 @@ void verify(List expected,CancellationToken token) { token.throwIfCancellationRequested(); var current=read(original.path().toUri().toString(),original.roots(),token); if(current.isEmpty() || !current.get().realPath().equals(original.realPath()) + || !current.get().fileKey().equals(original.fileKey()) || !current.get().loaded().byteSha256().equals(original.loaded().byteSha256())) throw error(ResponseErrorCode.ContentModified,"CONTENT_MODIFIED: closed source changed on disk"); if(!current.get().editable()) throw error(ResponseErrorCode.RequestFailed,"closed source is not editable"); diff --git a/interlis-lsp/src/test/java/guru/interlis/lsp/DocumentSynchronizationTest.java b/interlis-lsp/src/test/java/guru/interlis/lsp/DocumentSynchronizationTest.java index 2655b2d0..56105592 100644 --- a/interlis-lsp/src/test/java/guru/interlis/lsp/DocumentSynchronizationTest.java +++ b/interlis-lsp/src/test/java/guru/interlis/lsp/DocumentSynchronizationTest.java @@ -1,6 +1,8 @@ package guru.interlis.lsp; import guru.interlis.compiler.CompilerEngines; +import guru.interlis.compiler.CompilerServices; +import guru.interlis.execution.RunEvent; import guru.interlis.source.SourceId; import guru.interlis.workspace.*; import org.eclipse.lsp4j.*; @@ -10,6 +12,7 @@ import java.util.List; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.*; @@ -83,10 +86,102 @@ class DocumentSynchronizationTest { List.of(new TextDocumentContentChangeEvent(TEXT)))); assertEquals(8, publications.poll(5, TimeUnit.SECONDS).getVersion()); assertEquals(revision, workspace.currentRevision()); + + server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams( + new TextDocumentItem(URI, "interlis", 8, TEXT + "!! stale reopen\n"))); + assertEquals(TEXT, workspace.source(new SourceId(URI)).orElseThrow().text()); + assertEquals(revision, workspace.currentRevision()); } finally { server.shutdown().get(5, TimeUnit.SECONDS); } } } + @Test void diagnosticsForANewerCoreRevisionDoNotBecomeUnversionedOpenDiagnostics() throws Exception { + var workspace = Workspaces.create(CompilerEngines.create(), WorkspaceConfiguration.defaults()); + var server = new InterlisLanguageServer(workspace); + server.initialize(new InitializeParams()).get(5, TimeUnit.SECONDS); + var publications = new LinkedBlockingQueue(); + server.textDocumentsForTest().sink(publications::add); + try { + var service = server.getTextDocumentService(); + var sourceId = new SourceId(URI); + service.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(URI, "interlis", 7, TEXT))); + var opened = publications.poll(5, TimeUnit.SECONDS); + assertNotNull(opened); + assertEquals(7, opened.getVersion()); + var revision = workspace.source(sourceId).orElseThrow().revision(); + workspace.update(new SourceUpdate(sourceId, new SourceExpectation.At(revision), + new SourceChange.Replace(TEXT.replace("= END F.", "= DOMAIN D = Missing; END F.")))); + + String otherUri = "memory:/other.ili"; + String otherText = TEXT.replace("MODEL F", "MODEL G").replace("END F.", "END G."); + service.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(otherUri, "interlis", 2, otherText))); + assertFalse(service.documentSymbol(new DocumentSymbolParams(new TextDocumentIdentifier(otherUri))) + .get(5, TimeUnit.SECONDS).isEmpty()); + assertTrue(publications.stream().anyMatch(p -> otherUri.equals(p.getUri()) && Integer.valueOf(2).equals(p.getVersion()))); + assertTrue(publications.stream().noneMatch(p -> URI.equals(p.getUri()) && !p.getDiagnostics().isEmpty())); + assertEquals(ResponseErrorCode.ContentModified.getValue(), error(() -> service.documentSymbol( + new DocumentSymbolParams(new TextDocumentIdentifier(URI)))).getResponseError().getCode()); + } finally { server.shutdown().get(5, TimeUnit.SECONDS); } + } + + @Test void closingWhileAnalysisIsInFlightDiscardsTheAnswerAndAllowsANewVersionSequence() throws Exception { + var oldAnalysisComputed = new CountDownLatch(1); + var releaseOldAnalysis = new CountDownLatch(1); + var holdFirstAnalysis = new AtomicBoolean(true); + var defaults = CompilerServices.defaults(); + var services = new CompilerServices(event -> { + if (event instanceof RunEvent.Finished && holdFirstAnalysis.compareAndSet(true, false)) { + oldAnalysisComputed.countDown(); + try { + assertTrue(releaseOldAnalysis.await(10, TimeUnit.SECONDS)); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + } + } + }, defaults.clock(), defaults.runContexts()); + var workspace = Workspaces.create(CompilerEngines.create(), WorkspaceConfiguration.defaults()); + var server = new InterlisLanguageServer(workspace, CompilerEngines.create(services), services); + server.initialize(new InitializeParams()).get(5, TimeUnit.SECONDS); + var publications = new LinkedBlockingQueue(); + server.textDocumentsForTest().sink(publications::add); + try { + var service = server.getTextDocumentService(); + var document = new TextDocumentIdentifier(URI); + var sourceId = new SourceId(URI); + var symbols = new DocumentSymbolParams(document); + String broken = TEXT.replace("= END F.", "= DOMAIN D = Missing; END F."); + service.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(URI, "interlis", 7, broken))); + assertTrue(oldAnalysisComputed.await(5, TimeUnit.SECONDS)); + var oldRevision = workspace.source(sourceId).orElseThrow().revision(); + var obsolete = service.documentSymbol(symbols); + + service.didClose(new DidCloseTextDocumentParams(document)); + var failure = assertThrows(ExecutionException.class, () -> obsolete.get(5, TimeUnit.SECONDS)); + assertEquals(ResponseErrorCode.ContentModified.getValue(), + assertInstanceOf(ResponseErrorException.class, failure.getCause()).getResponseError().getCode()); + assertTrue(workspace.source(sourceId).isEmpty()); + var cleared = publications.poll(5, TimeUnit.SECONDS); + assertNotNull(cleared); + assertEquals(URI, cleared.getUri()); + assertNull(cleared.getVersion()); + assertTrue(cleared.getDiagnostics().isEmpty()); + + String reopened = TEXT.replace("MODEL F", "MODEL Reopened").replace("END F.", "END Reopened."); + service.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(URI, "interlis", 1, reopened))); + var current = service.documentSymbol(symbols).get(5, TimeUnit.SECONDS); + assertEquals(List.of("Reopened"), current.stream().map(symbol -> symbol.getRight().getName()).toList()); + assertTrue(workspace.source(sourceId).orElseThrow().revision().value() > oldRevision.value()); + var accepted = publications.poll(5, TimeUnit.SECONDS); + assertNotNull(accepted); + assertEquals(URI, accepted.getUri()); + assertEquals(1, accepted.getVersion()); + assertTrue(accepted.getDiagnostics().isEmpty()); + } finally { + releaseOldAnalysis.countDown(); + server.shutdown().get(5, TimeUnit.SECONDS); + } + } + @Test void anUnknownDocumentIsAnInvalidRequestWithoutInternalFailureData() throws Exception { var server = new InterlisLanguageServer(Workspaces.create(CompilerEngines.create(), WorkspaceConfiguration.defaults())); server.initialize(new org.eclipse.lsp4j.InitializeParams()).join(); @@ -123,9 +218,21 @@ class DocumentSynchronizationTest { assertTrue(messages.stream().noneMatch(message->message.contains("SECRET_PAYLOAD"))); assertEquals(opened?TEXT:null,workspace.source(new SourceId(URI)).map(value->value.text()).orElse(null)); service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(URI,3),List.of(new TextDocumentContentChangeEvent(new Range(new Position(0,0),new Position(0,0)),"ignored")))); + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(URI, 3), + List.of(new TextDocumentContentChangeEvent(TEXT + "!! stale full replacement\n")))); + assertEquals(ResponseErrorCode.ContentModified.getValue(), error(() -> service.documentSymbol( + new DocumentSymbolParams(new TextDocumentIdentifier(URI)))).getResponseError().getCode()); + assertEquals(opened ? TEXT : null, workspace.source(new SourceId(URI)).map(value -> value.text()).orElse(null)); + + var publications = new LinkedBlockingQueue(); + server.textDocumentsForTest().sink(publications::add); service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(URI,4),List.of(new TextDocumentContentChangeEvent(TEXT)))); assertFalse(service.documentSymbol(new DocumentSymbolParams(new TextDocumentIdentifier(URI))).get(5,TimeUnit.SECONDS).isEmpty()); assertEquals(TEXT,workspace.source(new SourceId(URI)).orElseThrow().text()); + var accepted = publications.poll(5, TimeUnit.SECONDS); + assertNotNull(accepted); + assertEquals(URI, accepted.getUri()); + assertEquals(4, accepted.getVersion()); } finally { server.shutdown().get(5,TimeUnit.SECONDS); } } } diff --git a/interlis-lsp/src/test/java/guru/interlis/lsp/LspFileIdentityRegistryTest.java b/interlis-lsp/src/test/java/guru/interlis/lsp/LspFileIdentityRegistryTest.java new file mode 100644 index 00000000..983a091f --- /dev/null +++ b/interlis-lsp/src/test/java/guru/interlis/lsp/LspFileIdentityRegistryTest.java @@ -0,0 +1,66 @@ +package guru.interlis.lsp; + +import guru.interlis.execution.CancellationToken; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LspFileIdentityRegistryTest { + @TempDir Path temporary; + + @Test void aliasPathLimitCountsBothPathsAndRejectsOverflowAtomically() { + var registry = new LspFileIdentityRegistry(); + var primary = temporary.resolve("Primary.ili"); + String canonical = primary.toUri().toString(); + registry.rememberPhysicalIdentity(new ProjectSourceLoader.FileIdentity(primary, primary, Optional.empty()), canonical); + // Leave room for exactly one additional path. + for (int i = 0; i < 39_998; i++) { + var alias = temporary.resolve("alias-" + i + ".ili"); + registry.rememberPhysicalIdentity(new ProjectSourceLoader.FileIdentity(alias, primary, Optional.empty()), canonical); + } + + var unknownAlias = temporary.resolve("unregistered-alias.ili"); + var unknownReal = temporary.resolve("unregistered-real.ili"); + var twoNewPaths = new ProjectSourceLoader.FileIdentity(unknownAlias, unknownReal, Optional.empty()); + var failure = assertThrows(ResponseErrorException.class, () -> registry.rememberPhysicalIdentity(twoNewPaths, canonical)); + assertEquals(ResponseErrorCode.RequestFailed.getValue(), failure.getResponseError().getCode()); + assertEquals(unknownAlias.toUri().toString(), registry.canonicalUri(unknownAlias.toUri().toString())); + assertEquals(unknownReal.toUri().toString(), registry.canonicalUri(unknownReal.toUri().toString())); + + var lastAlias = temporary.resolve("last-alias.ili"); + var last = new ProjectSourceLoader.FileIdentity(lastAlias, primary, Optional.empty()); + registry.rememberPhysicalIdentity(last, canonical); + registry.rememberPhysicalIdentity(last, canonical); + assertEquals(canonical, registry.canonicalUri(lastAlias.toUri().toString())); + var oneNewPath = new ProjectSourceLoader.FileIdentity(unknownAlias, primary, Optional.empty()); + assertThrows(ResponseErrorException.class, () -> registry.rememberPhysicalIdentity(oneNewPath, canonical)); + assertEquals(canonical, registry.canonicalUri(lastAlias.toUri().toString())); + } + + @Test void physicalIdentityWithoutFileKeysDistinguishesHardlinksFromEqualBytes() throws Exception { + var primary = Files.writeString(temporary.resolve("Base.ili"), + "INTERLIS 2.4; MODEL Base AT \"x\" VERSION \"1\" = END Base."); + var hardlink = Files.createLink(temporary.resolve("Link.ili"), primary); + var copy = Files.copy(primary, temporary.resolve("Copy.ili")); + var loader = new ProjectSourceLoader(); + var token = CancellationToken.none(); + var loaded = loader.read(primary.toUri().toString(), List.of(temporary.toRealPath()), token).orElseThrow(); + var keyless = new ProjectSourceLoader.FileContent(loaded.path(), loaded.realPath(), Optional.empty(), + loaded.loaded(), loaded.editable(), loaded.roots(), loaded.symbolicLink()); + + assertEquals(-1, Files.mismatch(primary, copy)); + assertTrue(loader.sameFile(keyless, hardlink.toUri().toString(), token)); + assertFalse(loader.sameFile(keyless, copy.toUri().toString(), token)); + } +} diff --git a/interlis-lsp/src/test/java/guru/interlis/lsp/LspProtocolCancellationTest.java b/interlis-lsp/src/test/java/guru/interlis/lsp/LspProtocolCancellationTest.java index 882f805a..9e432f67 100644 --- a/interlis-lsp/src/test/java/guru/interlis/lsp/LspProtocolCancellationTest.java +++ b/interlis-lsp/src/test/java/guru/interlis/lsp/LspProtocolCancellationTest.java @@ -10,6 +10,7 @@ import guru.interlis.workspace.WorkspaceConfiguration; import guru.interlis.workspace.Workspaces; import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.ExecuteCommandParams; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializedParams; @@ -17,15 +18,17 @@ import org.eclipse.lsp4j.MessageParams; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.launch.LSPLauncher; import org.eclipse.lsp4j.services.LanguageClient; import org.junit.jupiter.api.Test; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -77,23 +80,22 @@ void cancelNotificationProducesRequestCancelledAndSessionSurvives() throws Excep } }); InterlisLanguageServer server = new InterlisLanguageServer(real, slow, CompilerServices.defaults()); - PipedOutputStream clientOut = new PipedOutputStream(); - PipedInputStream serverIn = new PipedInputStream(clientOut); - PipedOutputStream serverOut = new PipedOutputStream(); - PipedInputStream clientIn = new PipedInputStream(serverOut); - var serverLauncher = LSPLauncher.createServerLauncher(server, serverIn, serverOut); - server.connect(serverLauncher.getRemoteProxy()); - serverLauncher.startListening(); - var clientLauncher = LSPLauncher.createClientLauncher(new Client(), clientIn, clientOut); - clientLauncher.startListening(); - var endpoint = clientLauncher.getRemoteEndpoint(); - try { + try (var listener = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()); + var clientSocket = new Socket(listener.getInetAddress(), listener.getLocalPort()); + var serverSocket = listener.accept()) { + var serverLauncher = LSPLauncher.createServerLauncher(server, serverSocket.getInputStream(), serverSocket.getOutputStream()); + server.connect(serverLauncher.getRemoteProxy()); + serverLauncher.startListening(); + var clientLauncher = LSPLauncher.createClientLauncher(new Client(), clientSocket.getInputStream(), clientSocket.getOutputStream()); + clientLauncher.startListening(); + var endpoint = clientLauncher.getRemoteEndpoint(); endpoint.request("initialize", new InitializeParams()).get(5, TimeUnit.SECONDS); endpoint.notify("initialized", new InitializedParams()); String uri = "memory:/cancel.ili"; endpoint.notify("textDocument/didOpen", new DidOpenTextDocumentParams( new TextDocumentItem(uri, "interlis", 1, MODEL))); - Thread.sleep(100); + endpoint.request("textDocument/documentSymbol", new DocumentSymbolParams(new TextDocumentIdentifier(uri))) + .get(5, TimeUnit.SECONDS); var compile = endpoint.request("workspace/executeCommand", new ExecuteCommandParams("interlis.compile", List.of())); @@ -112,12 +114,7 @@ void cancelNotificationProducesRequestCancelledAndSessionSurvives() throws Excep endpoint.request("shutdown", null).get(5, TimeUnit.SECONDS); endpoint.notify("exit", null); } finally { - try { - clientOut.close(); - serverOut.close(); - } catch (java.io.IOException ignored) { - // Best-effort cleanup. - } + if (!server.shutdownRequested()) server.exit(); } } diff --git a/interlis-lsp/src/test/java/guru/interlis/lsp/ProjectRenameConsistencyTest.java b/interlis-lsp/src/test/java/guru/interlis/lsp/ProjectRenameConsistencyTest.java index bb8db679..9f6dba91 100644 --- a/interlis-lsp/src/test/java/guru/interlis/lsp/ProjectRenameConsistencyTest.java +++ b/interlis-lsp/src/test/java/guru/interlis/lsp/ProjectRenameConsistencyTest.java @@ -1,6 +1,7 @@ package guru.interlis.lsp; import guru.interlis.compiler.CompilerEngines; +import guru.interlis.execution.CancellationToken; import guru.interlis.source.*; import guru.interlis.workspace.*; import org.eclipse.lsp4j.*; @@ -62,6 +63,35 @@ class ProjectRenameConsistencyTest { } finally { server.shutdown().get(5,TimeUnit.SECONDS); } } + @Test void aRenameWaitingForDiskVerificationIsCancelledWhenItsBufferChanges() throws Exception { + Path folder = project(), root = folder.resolve("Root.ili"); + var server = open(folder, true, null, null); + var verification = new CompletableFuture(); + var verifying = new CountDownLatch(1); + try { + ready(server, root); + server.textDocumentsForTest().fileServices(uri -> { }, (files, token) -> { + verifying.countDown(); + return verification; + }, (uri, known) -> CompletableFuture.failedFuture(new AssertionError("unexpected identity lookup"))); + + String uri = root.toUri().toString(); + var result = server.getTextDocumentService().rename(new RenameParams(new TextDocumentIdentifier(uri), cursor(), "Renamed")); + assertTrue(verifying.await(5, TimeUnit.SECONDS)); + server.getTextDocumentService().didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri, 6), + List.of(new TextDocumentContentChangeEvent(ROOT + "\n!! newer buffer")))); + + var failure = assertThrows(ExecutionException.class, () -> result.get(5, TimeUnit.SECONDS)); + assertEquals(ResponseErrorCode.ContentModified.getValue(), + assertInstanceOf(ResponseErrorException.class, failure.getCause()).getResponseError().getCode()); + assertThrows(CancellationException.class, () -> verification.get(5, TimeUnit.SECONDS)); + ready(server, root); + } finally { + verification.cancel(false); + server.shutdown().get(5, TimeUnit.SECONDS); + } + } + @Test void aKnownSourceOutsideTheEditableProjectCannotReceiveAPartialRename() throws Exception { Path folder=project(),base=folder.resolve("Base.ili"),root=folder.resolve("Root.ili"); Files.delete(base); @@ -118,6 +148,35 @@ class ProjectRenameConsistencyTest { } finally { server.shutdown().get(5,TimeUnit.SECONDS); } } + @Test void removingTheLastClosedFileClearsItsDiagnosticsWithoutAnotherAnalysis() throws Exception { + Path folder = Files.createDirectory(temporary.resolve("last-file")); + Path file = Files.writeString(folder.resolve("Broken.ili"), + "INTERLIS 2.4; MODEL Broken AT \"x\" VERSION \"1\" = DOMAIN D = Missing; END Broken."); + String uri = file.toUri().toString(); + var publications = new LinkedBlockingQueue(); + var server = new InterlisLanguageServer(Workspaces.create(CompilerEngines.create(), WorkspaceConfiguration.defaults())); + server.textDocumentsForTest().sink(publications::add); + var initialize = new InitializeParams(); + initialize.setWorkspaceFolders(List.of(new WorkspaceFolder(folder.toUri().toString(), "Project"))); + server.initialize(initialize).get(5, TimeUnit.SECONDS); + try { + var first = publications.poll(5, TimeUnit.SECONDS); + assertNotNull(first); + assertEquals(uri, first.getUri()); + assertNull(first.getVersion()); + assertFalse(first.getDiagnostics().isEmpty()); + + Files.delete(file); + server.getWorkspaceService().didChangeWatchedFiles(new DidChangeWatchedFilesParams(List.of(new FileEvent(uri, FileChangeType.Deleted)))); + var clear = publications.poll(5, TimeUnit.SECONDS); + assertNotNull(clear); + assertEquals(uri, clear.getUri()); + assertNull(clear.getVersion()); + assertTrue(clear.getDiagnostics().isEmpty()); + assertTrue(server.workspaceForTest().state().sourceRevisions().isEmpty()); + } finally { server.shutdown().get(5, TimeUnit.SECONDS); } + } + @Test void watcherAliasesReferToTheExistingPhysicalSourceAndKeepOpenClientAuthority() throws Exception { Path folder=project(),alias=temporary.resolve("project-alias"); Files.createSymbolicLink(alias,folder); @@ -195,6 +254,115 @@ class ProjectRenameConsistencyTest { } finally { server.shutdown().get(5,TimeUnit.SECONDS); } } + @Test void anOlderWatcherResultThroughAnUnknownHardlinkCannotReplaceTheNewerPrimary() throws Exception { + Path folder = project(), base = folder.resolve("Base.ili"), root = folder.resolve("Root.ili"); + var server = open(folder, true, null, null); + try { + ready(server, root); + Path link = Files.createLink(folder.resolve("HardBase.ili"), base); + String primaryUri = base.toUri().toString(), aliasUri = link.toUri().toString(); + var primary = new SourceId(primaryUri); + var loader = new ProjectSourceLoader(); + var documents = server.textDocumentsForTest(); + var workspaceService = (InterlisWorkspaceService) server.getWorkspaceService(); + + var aliasRequest = workspaceService.beginFileReload(aliasUri, 100); + assertEquals(aliasUri, aliasRequest.canonicalUri()); + String oldText = BASE + "\n!! older watcher read"; + Files.writeString(base, oldText); + var older = loader.read(aliasUri, List.of(folder.toRealPath()), CancellationToken.none()).orElseThrow(); + assertEquals(oldText, older.loaded().text()); + assertTrue(loader.sameFile(older, primaryUri, CancellationToken.none())); + + var primaryRequest = workspaceService.beginFileReload(primaryUri, 101); + assertEquals(primaryUri, primaryRequest.canonicalUri()); + String newText = BASE + "\n!! newer watcher read"; + Files.writeString(base, newText); + var newer = loader.read(primaryUri, List.of(folder.toRealPath()), CancellationToken.none()); + documents.applyExternal(primaryUri, primaryUri, 101, newer, Optional.of(primary), false, + resolved -> workspaceService.claimFileRequest(primaryUri, resolved, 101)); + documents.applyExternal(aliasUri, aliasUri, 100, Optional.of(older), Optional.of(primary), false, + resolved -> workspaceService.claimFileRequest(aliasUri, resolved, 100)); + + ready(server, root); + assertEquals(newText, server.workspaceForTest().source(primary).orElseThrow().text()); + assertEquals(2, server.workspaceForTest().state().sourceRevisions().size()); + assertEquals(2, rename(server, root).getDocumentChanges().size()); + } finally { server.shutdown().get(5, TimeUnit.SECONDS); } + } + + @Test void aFileFailureDoesNotPoisonASupersedingRefreshWhileAnotherFileIsPending() throws Exception { + Path folder = project(), root = folder.resolve("Root.ili"); + var server = open(folder, true, null, null); + try { + ready(server, root); + String replaced = folder.resolve("Replaced.ili").toUri().toString(); + String pending = folder.resolve("Pending.ili").toUri().toString(); + var documents = server.textDocumentsForTest(); + var workspaceService = (InterlisWorkspaceService) server.getWorkspaceService(); + assertNotNull(workspaceService.beginFileReload(replaced, 100)); + assertNotNull(workspaceService.beginFileReload(pending, 101)); + documents.externalFailed(replaced, 100, LspFailures.error(ResponseErrorCode.RequestFailed, "prior file read failed")); + assertNotNull(workspaceService.beginFileReload(replaced, 102)); + var outline = server.getTextDocumentService().documentSymbol(new DocumentSymbolParams(new TextDocumentIdentifier(root.toUri().toString()))); + assertFalse(outline.isDone()); + + documents.applyExternal(pending, pending, 101, Optional.empty(), Optional.empty(), true, + resolved -> workspaceService.claimFileRequest(pending, resolved, 101)); + documents.applyExternal(replaced, replaced, 102, Optional.empty(), Optional.empty(), true, + resolved -> workspaceService.claimFileRequest(replaced, resolved, 102)); + assertFalse(outline.get(5, TimeUnit.SECONDS).isEmpty()); + } finally { server.shutdown().get(5, TimeUnit.SECONDS); } + } + + @Test void closingAStandaloneFileAfterPrimaryDeletionRetainsItsSurvivingHardlink() throws Exception { + Path base = Files.writeString(temporary.resolve("Base.ili"), BASE); + Path link = Files.createLink(temporary.resolve("HardBase.ili"), base); + String primaryUri = base.toUri().toString(); + var primary = new SourceId(primaryUri); + var server = new InterlisLanguageServer(Workspaces.create(CompilerEngines.create(), WorkspaceConfiguration.defaults())); + server.initialize(new InitializeParams()).get(5, TimeUnit.SECONDS); + try { + var textDocuments = server.getTextDocumentService(); + String openText = BASE + "\n!! unsaved primary"; + textDocuments.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(primaryUri, "interlis", 1, openText))); + textDocuments.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(link.toUri().toString(), "interlis", 1, BASE))); + ready(server, base); + var workspace = server.workspaceForTest(); + assertEquals(Set.of(primary), workspace.state().sourceRevisions().keySet()); + assertEquals(openText, workspace.source(primary).orElseThrow().text()); + + Files.delete(base); + textDocuments.didClose(new DidCloseTextDocumentParams(new TextDocumentIdentifier(primaryUri))); + var result = server.getWorkspaceService().executeCommand(new ExecuteCommandParams("interlis.compile", List.of())).get(5, TimeUnit.SECONDS); + assertEquals("succeeded", ((Map) result).get("outcome")); + assertEquals(Set.of(primary), workspace.state().sourceRevisions().keySet()); + assertEquals(BASE, workspace.source(primary).orElseThrow().text()); + } finally { server.shutdown().get(5, TimeUnit.SECONDS); } + } + + @Test void aDeletedHardlinkAddressCanOpenAsAnIndependentFile() throws Exception { + Path folder = project(), base = folder.resolve("Base.ili"), root = folder.resolve("Root.ili"); + Path link = Files.createLink(folder.resolve("HardBase.ili"), base); + var server = open(folder, true, null, null); + try { + ready(server, root); + String uri = link.toUri().toString(); + Files.delete(link); + server.getWorkspaceService().didChangeWatchedFiles(new DidChangeWatchedFilesParams(List.of(new FileEvent(uri, FileChangeType.Deleted)))); + ready(server, root); + + String distinct = BASE.replace("Base", "Distinct"); + Files.writeString(link, distinct); + server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(uri, "interlis", 1, distinct))); + ready(server, link); + assertEquals(distinct, server.workspaceForTest().source(new SourceId(uri)).orElseThrow().text()); + assertEquals(BASE, server.workspaceForTest().source(new SourceId(base.toUri().toString())).orElseThrow().text()); + assertEquals(3, server.workspaceForTest().state().sourceRevisions().size()); + assertEquals(2, rename(server, root).getDocumentChanges().size()); + } finally { server.shutdown().get(5, TimeUnit.SECONDS); } + } + @Test void oversizedProjectInputHasAControlledSourceLimitFailure() throws Exception { Path folder=Files.createDirectory(temporary.resolve("large-project")); Files.writeString(folder.resolve("Large.ili"),"INTERLIS 2.4; MODEL Large AT \"x\" VERSION \"1\" = END Large.\n!!"+"x".repeat(2_100_000)); @@ -313,6 +481,60 @@ class ProjectRenameConsistencyTest { } finally { server.shutdown().get(5,TimeUnit.SECONDS); } } + @Test void aFullReplacementCanRecoverAfterIdentityLookupRetriesAreExhausted() throws Exception { + Path folder = Files.createDirectory(temporary.resolve("known")); + Files.writeString(folder.resolve("Base.ili"), BASE); + String clientText = "INTERLIS 2.4; MODEL Client AT \"x\" VERSION \"1\" = END Client."; + Path client = Files.writeString(temporary.resolve("Client.ili"), clientText); + String uri = client.toUri().toString(); + var document = new TextDocumentIdentifier(uri); + var token = CancellationToken.none(); + var loader = new ProjectSourceLoader(); + var roots = List.of(folder.toUri().toString()); + var server = new InterlisLanguageServer(Workspaces.create(CompilerEngines.create(), WorkspaceConfiguration.defaults())); + server.initialize(new InitializeParams()).get(5, TimeUnit.SECONDS); + try { + var service = server.textDocumentsForTest(); + service.installProject(loader.scan(roots, token), ignored -> true); + record Lookup(Map known, CompletableFuture answer) { } + var lookups = new LinkedBlockingQueue(); + service.fileServices(ignored -> { }, (files, cancellation) -> CompletableFuture.completedFuture(null), + (address, known) -> { + var answer = new CompletableFuture(); + lookups.add(new Lookup(known, answer)); + return answer; + }); + service.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(uri, "interlis", 1, clientText))); + for (int i = 0; i < 4; i++) { + var lookup = lookups.poll(5, TimeUnit.SECONDS); + assertNotNull(lookup); + var match = loader.identify(uri, lookup.known(), token); + String name = "NewSource" + i; + Files.writeString(folder.resolve(name + ".ili"), + "INTERLIS 2.4; MODEL " + name + " AT \"x\" VERSION \"1\" = END " + name + "."); + service.installProject(loader.scan(roots, token), ignored -> true); + lookup.answer().complete(match); + } + var failed = assertThrows(ExecutionException.class, () -> + service.documentSymbol(new DocumentSymbolParams(document)).get(5, TimeUnit.SECONDS)); + assertEquals(ResponseErrorCode.ContentModified.getValue(), + assertInstanceOf(ResponseErrorException.class, failed.getCause()).getResponseError().getCode()); + assertTrue(server.workspaceForTest().source(new SourceId(uri)).isEmpty()); + + String replacement = clientText + "\n!! client full replacement"; + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri, 2), + List.of(new TextDocumentContentChangeEvent(replacement)))); + var recovery = lookups.poll(5, TimeUnit.SECONDS); + assertNotNull(recovery); + recovery.answer().complete(loader.identify(uri, recovery.known(), token)); + var symbols = service.documentSymbol(new DocumentSymbolParams(document)).get(5, TimeUnit.SECONDS); + assertEquals(List.of("Client"), symbols.stream().map(symbol -> symbol.getRight().getName()).toList()); + assertEquals(replacement, server.workspaceForTest().source(new SourceId(uri)).orElseThrow().text()); + } finally { + server.shutdown().get(5, TimeUnit.SECONDS); + } + } + @Test void primaryDeletionReportedThroughTheSurvivingAliasPreservesTheCoreIdentity() throws Exception { for(var kind:List.of(FileChangeType.Deleted,FileChangeType.Changed)) { Path folder=project(),base=folder.resolve("Base.ili"),root=folder.resolve("Root.ili"),alias=folder.resolve("HardBase.ili"); diff --git a/interlis-lsp/src/test/java/guru/interlis/lsp/RepositoryEditorTest.java b/interlis-lsp/src/test/java/guru/interlis/lsp/RepositoryEditorTest.java index f5059291..1181ddf9 100644 --- a/interlis-lsp/src/test/java/guru/interlis/lsp/RepositoryEditorTest.java +++ b/interlis-lsp/src/test/java/guru/interlis/lsp/RepositoryEditorTest.java @@ -6,6 +6,8 @@ import guru.interlis.source.*; import guru.interlis.workspace.*; import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.junit.jupiter.api.*; import org.junit.jupiter.api.io.TempDir; import java.net.*; @@ -27,6 +29,9 @@ class RepositoryEditorTest { static final String URI="untitled:Root.ili"; @AfterEach void stopHttp() { if(http!=null)http.stop(0); } String serve() throws Exception { + return serve(DEP); + } + private String serve(String dependency) throws Exception { http=HttpServer.create(new InetSocketAddress("127.0.0.1",0),0); String index=""+List.of("Dep","Base").stream().map(n -> ""+n+"ili2_41"+n+".ili").reduce("",String::concat)+""; http.createContext("/",e -> { @@ -35,7 +40,7 @@ String serve() throws Exception { try { if (!releaseIndex.await(5, TimeUnit.SECONDS)) throw new java.io.IOException("controlled index release timed out"); } catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new java.io.IOException(interrupted); } } - requests.incrementAndGet();String body=switch(e.getRequestURI().getPath()) {case "/ilimodels.xml" -> index;case "/Dep.ili" -> DEP;case "/Base.ili" -> BASE;default -> null;}; + requests.incrementAndGet();String body=switch(e.getRequestURI().getPath()) {case "/ilimodels.xml" -> index;case "/Dep.ili" -> dependency;case "/Base.ili" -> BASE;default -> null;}; if(body==null) {e.sendResponseHeaders(404,-1);e.close();return;} e.getResponseHeaders().set("ETag","\"frozen\""); if("\"frozen\"".equals(e.getRequestHeaders().getFirst("If-None-Match"))) {e.sendResponseHeaders(304,-1);e.close();return;} @@ -169,6 +174,63 @@ InterlisLanguageServer server(String base,boolean offline,Path cache) throws Exc } finally { s.shutdown().get(5,TimeUnit.SECONDS); } } + @Test void remoteDiagnosticsFollowOpenVersionsAndClearWhenTheHandleExpires() throws Exception { + String duplicate = "STRUCTURE Duplicate = END Duplicate;"; + String dependency = DEP.replace("DOMAIN Value=0..10;\n", + "DOMAIN Value=0..10;\n" + duplicate + "\n" + duplicate + "\n").replace("\n", "\r\n"); + var s = server(serve(dependency), false, directory.resolve("cache")); + var publications = new CopyOnWriteArrayList(); + s.textDocumentsForTest().sink(publications::add); + try { + open(s, URI, 7, ROOT); + String remoteUri = definitions(s, URI, 3, ROOT.split("\n")[3].indexOf("Value")).getFirst().getUri(); + assertTrue(remoteUri.startsWith("interlis-source:/")); + String revision = java.net.URI.create(remoteUri).getQuery(); + var content = s.sourceContent(new SourceContentParams(remoteUri, revision)).get(5, TimeUnit.SECONDS); + assertEquals(dependency, content.text()); + assertTrue(content.readonly()); + var closed = publications.stream().filter(p -> remoteUri.equals(p.getUri())).findFirst().orElseThrow(); + assertNull(closed.getVersion()); + var diagnostic = closed.getDiagnostics().stream() + .filter(d -> !d.getRelatedInformation().isEmpty()).findFirst().orElseThrow(); + assertEquals(new Range(new Position(5, 0), new Position(5, duplicate.length())), diagnostic.getRange()); + assertEquals(new Location(remoteUri, new Range(new Position(4, 10), new Position(4, 19))), + diagnostic.getRelatedInformation().getFirst().getLocation()); + + var service = s.getTextDocumentService(); + var remote = new TextDocumentIdentifier(remoteUri); + open(s, remoteUri, 11, content.text()); + service.documentSymbol(new DocumentSymbolParams(remote)).get(5, TimeUnit.SECONDS); + var opened = publications.stream().filter(p -> remoteUri.equals(p.getUri()) + && Integer.valueOf(11).equals(p.getVersion())).findFirst().orElseThrow(); + assertTrue(opened.getDiagnostics().contains(diagnostic)); + + int beforeClose = publications.size(); + service.didClose(new DidCloseTextDocumentParams(remote)); + var rootOutline = new DocumentSymbolParams(new TextDocumentIdentifier(URI)); + service.documentSymbol(rootOutline).get(5, TimeUnit.SECONDS); + var afterClose = publications.stream().skip(beforeClose).filter(p -> remoteUri.equals(p.getUri())).toList(); + assertTrue(afterClose.getFirst().getDiagnostics().isEmpty()); + assertTrue(afterClose.stream().allMatch(p -> p.getVersion() == null)); + assertTrue(afterClose.getLast().getDiagnostics().contains(diagnostic)); + + int beforeConfiguration = publications.size(); + s.getWorkspaceService().didChangeConfiguration(new DidChangeConfigurationParams( + Map.of("offline", true, "analysisBudget", Map.of("maxTokens", 1)))); + var invalidated = publications.stream().skip(beforeConfiguration) + .filter(p -> remoteUri.equals(p.getUri())).toList(); + assertEquals(1, invalidated.size()); + assertNull(invalidated.getFirst().getVersion()); + assertTrue(invalidated.getFirst().getDiagnostics().isEmpty()); + var unavailable = assertThrows(ExecutionException.class, + () -> service.documentSymbol(rootOutline).get(5, TimeUnit.SECONDS)); + assertEquals(ResponseErrorCode.RequestCancelled.getValue(), + assertInstanceOf(ResponseErrorException.class, unavailable.getCause()).getResponseError().getCode()); + assertThrows(ExecutionException.class, () -> + s.sourceContent(new SourceContentParams(remoteUri, revision)).get(5, TimeUnit.SECONDS)); + } finally { s.shutdown().get(5, TimeUnit.SECONDS); } + } + @Test void multipleModelsRequireExplicitSelectionAndExactPinsAreCheckedForLoadedSources() throws Exception { var s=server("",false,directory.resolve("cache")); String valid="INTERLIS 2.4; MODEL Good AT \"x\" VERSION \"1\" = END Good.\n"; diff --git a/interlis-lsp/src/test/java/guru/interlis/lsp/StaleAnalysisSchedulingTest.java b/interlis-lsp/src/test/java/guru/interlis/lsp/StaleAnalysisSchedulingTest.java index f4eec967..24dc0fe5 100644 --- a/interlis-lsp/src/test/java/guru/interlis/lsp/StaleAnalysisSchedulingTest.java +++ b/interlis-lsp/src/test/java/guru/interlis/lsp/StaleAnalysisSchedulingTest.java @@ -2,12 +2,20 @@ import guru.interlis.compiler.*; import guru.interlis.execution.*; +import guru.interlis.source.SourceId; import guru.interlis.workspace.*; import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.lang.reflect.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import static org.junit.jupiter.api.Assertions.*; @@ -19,7 +27,7 @@ class StaleAnalysisSchedulingTest { @Test void detachedLanguageWorkDoesNotBlockEditsAndCannotReturnAnOldAnswer() throws Exception { var workspace=Workspaces.create(CompilerEngines.create(),WorkspaceConfiguration.defaults()); - var scheduler=new AnalysisScheduler(workspace,CompilerEngines.create(),CompilerServices.defaults(),ignored -> { },ignored -> { }); + var scheduler=new AnalysisScheduler(workspace,CompilerEngines.create(),CompilerServices.defaults(),(analysis, repositoryFailure) -> { },ignored -> { }); var entered=new CountDownLatch(1); var release=new CountDownLatch(1); var ownerCaller=Executors.newSingleThreadExecutor(); try { @@ -46,7 +54,7 @@ class StaleAnalysisSchedulingTest { @Test void shutdownCancelsActiveLanguageWorkAndCompletesItsRequest() throws Exception { var workspace=Workspaces.create(CompilerEngines.create(),WorkspaceConfiguration.defaults()); - var scheduler=new AnalysisScheduler(workspace,CompilerEngines.create(),CompilerServices.defaults(),ignored -> { },ignored -> { }); + var scheduler=new AnalysisScheduler(workspace,CompilerEngines.create(),CompilerServices.defaults(),(analysis, repositoryFailure) -> { },ignored -> { }); var entered=new CountDownLatch(1); var ended=new CountDownLatch(1); var reason=new AtomicReference(); try { @@ -63,6 +71,92 @@ class StaleAnalysisSchedulingTest { } finally { scheduler.close();workspace.close(); } } + @Test void fileWaitersReleaseCancelledSlotsAndCompleteOnLoadOrShutdown(@TempDir Path temporary) throws Exception { + var workspace = Workspaces.create(CompilerEngines.create(), WorkspaceConfiguration.defaults()); + var server = new InterlisLanguageServer(workspace); + server.initialize(new InitializeParams()).get(5, TimeUnit.SECONDS); + try { + var documents = server.textDocumentsForTest(); + var outline = new DocumentSymbolParams(new TextDocumentIdentifier(URI)); + documents.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(URI, "interlis", 1, TEXT))); + assertFalse(documents.documentSymbol(outline).get(5, TimeUnit.SECONDS).isEmpty()); + var file = Files.writeString(temporary.resolve("Dependency.ili"), + TEXT.replace("MODEL F", "MODEL Dependency").replace("END F.", "END Dependency.")); + String fileUri = file.toUri().toString(); + documents.beginExternal(fileUri, 1); + + var accepted = new ArrayList>(); + for (int i = 0; i < 128; i++) accepted.add(documents.documentSymbol(outline)); + assertTrue(accepted.stream().noneMatch(CompletableFuture::isDone)); + assertRequestFailure(documents.documentSymbol(outline), ResponseErrorCode.RequestFailed); + + var cancelled = accepted.removeFirst(); + assertTrue(cancelled.cancel(false)); + assertThrows(CancellationException.class, () -> cancelled.get(5, TimeUnit.SECONDS)); + accepted.add(documents.documentSymbol(outline)); + assertFalse(accepted.getLast().isDone()); + assertRequestFailure(documents.documentSymbol(outline), ResponseErrorCode.RequestFailed); + + var loaded = new ProjectSourceLoader().read(fileUri, List.of(temporary.toRealPath()), + CancellationToken.none()).orElseThrow(); + documents.applyExternal(fileUri, fileUri, 1, Optional.of(loaded), Optional.empty(), false, ignored -> true); + for (var request : accepted) request.get(5, TimeUnit.SECONDS); + assertTrue(workspace.source(new SourceId(fileUri)).isPresent()); + assertTrue(cancelled.isCancelled()); + + documents.beginExternal(fileUri, 2); + var awaitingShutdown = documents.documentSymbol(outline); + assertFalse(awaitingShutdown.isDone()); + server.shutdown().get(5, TimeUnit.SECONDS); + assertRequestFailure(awaitingShutdown, ResponseErrorCode.RequestCancelled); + } finally { + if (!server.shutdownRequested()) server.shutdown().get(5, TimeUnit.SECONDS); + } + } + + @Test void identityLookupBoundsDeferredDocumentOperationsAndReleasesThemInOrder(@TempDir Path temporary) throws Exception { + var known = Files.createDirectory(temporary.resolve("known")); + Files.writeString(known.resolve("Dependency.ili"), + TEXT.replace("MODEL F", "MODEL Dependency").replace("END F.", "END Dependency.")); + var workspace = Workspaces.create(CompilerEngines.create(), WorkspaceConfiguration.defaults()); + var server = new InterlisLanguageServer(workspace); + server.initialize(new InitializeParams()).get(5, TimeUnit.SECONDS); + var lookup = new CompletableFuture(); + try { + var documents = server.textDocumentsForTest(); + documents.installProject(new ProjectSourceLoader().scan(List.of(known.toUri().toString()), + CancellationToken.none()), ignored -> true); + var lookupStarted = new CountDownLatch(1); + documents.fileServices(ignored -> { }, (files, token) -> CompletableFuture.completedFuture(null), + (uri, sources) -> { + lookupStarted.countDown(); + return lookup; + }); + String uri = temporary.resolve("Unsaved.ili").toUri().toString(); + var document = new TextDocumentIdentifier(uri); + documents.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(uri, "interlis", 1, TEXT))); + assertTrue(lookupStarted.await(5, TimeUnit.SECONDS)); + + var stale = new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri, 1), + List.of(new TextDocumentContentChangeEvent(TEXT + "\n!! stale"))); + for (int i = 0; i < 255; i++) documents.didChange(stale); + String replacement = TEXT + "\n!! last admitted operation"; + documents.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri, 2), + List.of(new TextDocumentContentChangeEvent(replacement)))); + var overflow = assertThrows(ResponseErrorException.class, () -> + documents.didClose(new DidCloseTextDocumentParams(document))); + assertEquals(ResponseErrorCode.RequestFailed.getValue(), overflow.getResponseError().getCode()); + assertTrue(workspace.source(new SourceId(uri)).isEmpty()); + + lookup.complete(new ProjectSourceLoader.IdentityMatch(Optional.empty(), Optional.empty())); + assertFalse(documents.documentSymbol(new DocumentSymbolParams(document)).get(5, TimeUnit.SECONDS).isEmpty()); + assertEquals(replacement, workspace.source(new SourceId(uri)).orElseThrow().text()); + } finally { + server.shutdown().get(5, TimeUnit.SECONDS); + lookup.cancel(false); + } + } + @Test void anObsoleteCompilerCannotBlockNotificationsOrPublishAfterANewerGeneration() throws Exception { var entered = new CountDownLatch(1); var release = new CountDownLatch(1); var cancellations = new AtomicBoolean(); var analysisCalls = new AtomicInteger(); @@ -114,4 +208,10 @@ class StaleAnalysisSchedulingTest { release.countDown(); notifications.shutdownNow(); server.shutdown().get(5, TimeUnit.SECONDS); } } + + private static void assertRequestFailure(CompletableFuture request, ResponseErrorCode expected) { + var failure = assertThrows(ExecutionException.class, () -> request.get(5, TimeUnit.SECONDS)); + assertEquals(expected.getValue(), + assertInstanceOf(ResponseErrorException.class, failure.getCause()).getResponseError().getCode()); + } } diff --git a/testdata/verification/compiler-contracts.json b/testdata/verification/compiler-contracts.json index 8381ba12..1ec4f269 100644 --- a/testdata/verification/compiler-contracts.json +++ b/testdata/verification/compiler-contracts.json @@ -4392,7 +4392,7 @@ "processEvidence": [ { "source": "pom.xml", - "sha256": "2ef3154aff03683fd024cfb96f399f371a94937c56ea1f4905dd3be2013271ba", + "sha256": "f6486fe39006340ee10fbc3a3d57f85ff8af574ec23966c248fb7f32a98f456a", "reason": "Auftrag B: JVM-POM-/Profilbesitz und explizite Jenesis-Reportpfade; fachliche Assertion-/Compilerverträge bleiben erhalten. Historischer Originaleintrag unverändert." }, { @@ -4791,7 +4791,7 @@ }, { "source": "interlis-compiler/src/main/java/guru/interlis/compiler/internal/CompilationRun.java", - "sha256": "c200390bc138e583a7940572e9a794e165cb23170f0da5b3a9a21650da38f71c" + "sha256": "e9da1945fde8e05f12ddac290f77aab4294c11097bd929c9c51602abc6eb9138" } ], "resolution": "Keine feingranulare semantische Memoisierung aktiviert: die Pipeline baut den Graph stets vollständig neu; Syntax-/Identitätsreuse und Hostcache sind getrennt. Benchmarkbedingung vor einer künftigen semantischen Optimierung bleibt bestehen." @@ -5063,7 +5063,7 @@ }, { "source": "interlis-lsp/src/main/java/guru/interlis/lsp/InterlisLanguageServer.java", - "sha256": "ec620485ec75c3ed756b95a9655a4b242022f17fbf60df35b471d2f946ab0beb", + "sha256": "1afa7396bca7f9dc55d268131c9d0277e5a5fba587ba7cb01847bb8dcca2400a", "reason": "Current base API/build documentation drift or verification task integration; no new semantic acceptance" } ], @@ -5207,7 +5207,7 @@ }, { "source": "docs/contracts/requirements.md", - "sha256": "fa64094305cd364535e9409582d229f206e27eab46b8c061e9973ec1f15661de", + "sha256": "4a049a378365699cb3f0ba992fc245ca4c5ba079d3093c2067c3bd0a5f6bc954", "reason": "Auftrag B: JVM-POM-/Profilbesitz und explizite Jenesis-Reportpfade; fachliche Assertion-/Compilerverträge bleiben erhalten. Historischer Originaleintrag unverändert." } ], @@ -12209,10 +12209,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/AssociationAccessContractTest.java", "suite": "guru.interlis.compiler.AssociationAccessContractTest", "method": "inheritsAccessThroughClassIdentityAndHiding", - "methodStart": 12025, - "methodEnd": 13120, + "methodStart": 13507, + "methodEnd": 14602, "methodSha256": "d8413ee754f79f4180bf5e09c92e89b342a3f074a9fda60701f571095a959d1c", - "sourceSha256": "1620c8a2297dac4518d02f702f4b181f2face85b469ba54d4370de2b76ad8331", + "sourceSha256": "060c27be97e6a43c4e8d29fe1372a09ca5b7be135dc8cc1c776396e8abb17409", "observations": [ "access/hiding" ], @@ -12279,10 +12279,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/AssociationAccessContractTest.java", "suite": "guru.interlis.compiler.AssociationAccessContractTest", "method": "distinguishesClassAccessFromDirectAssociationRoleAndRetainsCardinality", - "methodStart": 5437, - "methodEnd": 7510, + "methodStart": 5490, + "methodEnd": 7563, "methodSha256": "84e74f92f89184862928d1243338e853d1418002ef91bc557ad76a7941ea69e8", - "sourceSha256": "1620c8a2297dac4518d02f702f4b181f2face85b469ba54d4370de2b76ad8331", + "sourceSha256": "060c27be97e6a43c4e8d29fe1372a09ca5b7be135dc8cc1c776396e8abb17409", "observations": [ "role vs association access", "access" @@ -12514,10 +12514,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/PredefinedBindingContractTest.java", "suite": "guru.interlis.compiler.PredefinedBindingContractTest", "method": "predefinedDomainsRetainTypesFinalityOriginAndNavigationTargets", - "methodStart": 3990, - "methodEnd": 6791, + "methodStart": 4089, + "methodEnd": 6890, "methodSha256": "6d965e0e0923a43432b88bb21131905bf03b94453243fa4bf958c4857d5a4a25", - "sourceSha256": "6f567643e5f74424a1a3ac842c18d97332fe42942924724ad16124df914cca67", + "sourceSha256": "d393008f1c2ad03e9a49e4e3728c3acd3a8d59e4055d65d06263337cc7b70af0", "observations": [ "HALIGNMENT/VALIGNMENT predefined domains", "HALIGNMENT/VALIGNMENT", @@ -13193,7 +13193,7 @@ "methodStart": 4195, "methodEnd": 6039, "methodSha256": "1f551fb1bd5d3e0229754340ff7740a407a4b16bf103a09d8ee6042bbebe921e", - "sourceSha256": "92fa68cdaca0a5e2250604c7cc4defc8e7484ab8685f18348628724e5ddce086", + "sourceSha256": "788770e210325f93a625eb6407ae03a1000580b318347a648a894e49804b495a", "observations": [ "graphic metaobjects", "GRAPHIC BASED ON/rules", @@ -13232,7 +13232,7 @@ "methodStart": 6045, "methodEnd": 7371, "methodSha256": "854dca62f1f796b1ea477755696cdf1e0b7553300f04bc965622a3a2dd850933", - "sourceSha256": "92fa68cdaca0a5e2250604c7cc4defc8e7484ab8685f18348628724e5ddce086", + "sourceSha256": "788770e210325f93a625eb6407ae03a1000580b318347a648a894e49804b495a", "observations": [ "signature parameters", "free signature parameters", @@ -13617,10 +13617,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/ViewBindingContractTest.java", "suite": "guru.interlis.compiler.ViewBindingContractTest", "method": "joinsAndUnionsKeepAllBasesAndCheckMandatoryValues", - "methodStart": 5356, - "methodEnd": 6677, + "methodStart": 6602, + "methodEnd": 7923, "methodSha256": "c719b15cce340b14c691a7ea03d88ecdf9a866236a2a72403a8b43f44c80fe3b", - "sourceSha256": "e8bcf7ef011cb414a7315035cb76edb498241fedee88fa1facc39e2a7f0ddb4a", + "sourceSha256": "82bfa52c97fd1e4567ea84c7ff44a9625334ad9ee24c6d65dbf0a493a411e7da", "observations": [ "JOIN/UNION" ], @@ -13634,10 +13634,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/ViewBindingContractTest.java", "suite": "guru.interlis.compiler.ViewBindingContractTest", "method": "aggregationExposesItsBagWithoutMakingItATransferMember", - "methodStart": 6682, - "methodEnd": 7704, + "methodStart": 7928, + "methodEnd": 8950, "methodSha256": "0d30bf6a88eb787645a450c3ba7c0363844ef5bb12eee470a214ab6d1ee04d40", - "sourceSha256": "e8bcf7ef011cb414a7315035cb76edb498241fedee88fa1facc39e2a7f0ddb4a", + "sourceSha256": "82bfa52c97fd1e4567ea84c7ff44a9625334ad9ee24c6d65dbf0a493a411e7da", "observations": [ "AGGREGATION" ], @@ -13688,7 +13688,7 @@ "methodStart": 7377, "methodEnd": 8482, "methodSha256": "85a5e91eaf08b86c9e0868e845bf2b0b3dc9b049c62f57792c1c52708360ff8a", - "sourceSha256": "92fa68cdaca0a5e2250604c7cc4defc8e7484ab8685f18348628724e5ddce086", + "sourceSha256": "788770e210325f93a625eb6407ae03a1000580b318347a648a894e49804b495a", "observations": [ "WHERE/assignments" ], @@ -13723,7 +13723,7 @@ "methodStart": 9528, "methodEnd": 10627, "methodSha256": "41fa2c65d9b37bb6ade145917e8ca5254bd3a8a6583ad6efc4088e92584b09bd", - "sourceSha256": "92fa68cdaca0a5e2250604c7cc4defc8e7484ab8685f18348628724e5ddce086", + "sourceSha256": "788770e210325f93a625eb6407ae03a1000580b318347a648a894e49804b495a", "observations": [ "graphic inheritance", "inheritance" @@ -13932,7 +13932,7 @@ "methodStart": 8488, "methodEnd": 9522, "methodSha256": "eb1f39df471fb8464da8cd63f36dad3415b2e1557b0fe1f955721fc8d16f9024", - "sourceSha256": "92fa68cdaca0a5e2250604c7cc4defc8e7484ab8685f18348628724e5ddce086", + "sourceSha256": "788770e210325f93a625eb6407ae03a1000580b318347a648a894e49804b495a", "observations": [ "ACCORDING/WHEN IN" ], @@ -14170,10 +14170,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/ScopeNamespaceContractTest.java", "suite": "guru.interlis.compiler.ScopeNamespaceContractTest", "method": "duplicateTypeNamesAreRejectedInEveryDeclaredScopeEvenWithoutAnyUse", - "methodStart": 681, - "methodEnd": 1677, + "methodStart": 852, + "methodEnd": 1848, "methodSha256": "97ef2c61ef7e0f9c5ae4e35542d9adaeb8235e8b6f7c4cc1b76319441495adee", - "sourceSha256": "4ffe1de54a26ec5d0bbd5258ef4a88dea761729324ca4b105ae8ccc00786ff98", + "sourceSha256": "10e7c53da334db1ac996961cb3187ffeaaed0641e975251d329dc47c4c0cabaa", "observations": [ "Unused duplicate structures and cross-kind type names at model/topic levels are source errors" ], @@ -14187,10 +14187,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/ScopeNamespaceContractTest.java", "suite": "guru.interlis.compiler.ScopeNamespaceContractTest", "method": "localTypesCannotShadowOuterTypesOrUnmarkedInheritedTypes", - "methodStart": 2669, - "methodEnd": 3648, + "methodStart": 3922, + "methodEnd": 4901, "methodSha256": "6ed67a835c0471656273f1e9e01771ecc86060de4974851378c2c9c248097a1c", - "sourceSha256": "4ffe1de54a26ec5d0bbd5258ef4a88dea761729324ca4b105ae8ccc00786ff98", + "sourceSha256": "10e7c53da334db1ac996961cb3187ffeaaed0641e975251d329dc47c4c0cabaa", "observations": [ "Approved strict outer and inherited type-name collisions give SOURCE_ERRORS" ], @@ -14204,10 +14204,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/ScopeNamespaceContractTest.java", "suite": "guru.interlis.compiler.ScopeNamespaceContractTest", "method": "qualifiedOuterReferencesAndExplicitInheritedExtensionsKeepTheirActualTargets", - "methodStart": 3653, - "methodEnd": 5242, + "methodStart": 4906, + "methodEnd": 6495, "methodSha256": "ebfd8a5d811b3256614b7798d34cf0ca84081038812b6a54ba1d47fcc0f66e9c", - "sourceSha256": "4ffe1de54a26ec5d0bbd5258ef4a88dea761729324ca4b105ae8ccc00786ff98", + "sourceSha256": "10e7c53da334db1ac996961cb3187ffeaaed0641e975251d329dc47c4c0cabaa", "observations": [ "Qualified domain target and valid EXTENDED structure base/member order" ], @@ -14478,10 +14478,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/CompilerEngineTest.java", "suite": "guru.interlis.compiler.CompilerEngineTest", "method": "trailingContentRejectsCompilation", - "methodStart": 7835, - "methodEnd": 8724, + "methodStart": 12672, + "methodEnd": 13561, "methodSha256": "d5e4fff0fc6cc78f468b924e386195375685e7a95315a6aaa403b9da9ad3653d", - "sourceSha256": "d50382816beed7527cd530fbe9092420fde4f8b0d04c184ac4f39111c918405a", + "sourceSha256": "5ce6ed2d366adbb89382d797282164c3a9d8fdffa8b24f5c14c04c0443ff0067", "observations": [ "trailing content rejected" ], @@ -14782,10 +14782,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/CompilerEngineTest.java", "suite": "guru.interlis.compiler.CompilerEngineTest", "method": "unrelatedBrokenModelDoesNotBlock", - "methodStart": 6294, - "methodEnd": 7829, + "methodStart": 11131, + "methodEnd": 12666, "methodSha256": "81d3e05cb42baaf86494c93847bdcdbe503a3c01ebd3ae495fa9046a9196c34a", - "sourceSha256": "d50382816beed7527cd530fbe9092420fde4f8b0d04c184ac4f39111c918405a", + "sourceSha256": "5ce6ed2d366adbb89382d797282164c3a9d8fdffa8b24f5c14c04c0443ff0067", "observations": [ "broken unrelated model does not block", "two roots, one invalid" @@ -15857,10 +15857,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/LifecycleBoundaryContractTest.java", "suite": "guru.interlis.compiler.LifecycleBoundaryContractTest", "method": "repeatedMalformedSourcesAndDeeplyNestedValidSourcesTerminateWithExactOutcomeKinds", - "methodStart": 3048, - "methodEnd": 4547, + "methodStart": 4635, + "methodEnd": 6134, "methodSha256": "474608da456f61e0ab6ea10131939ad815dff54d1621c8573f2e27e8eb22bff0", - "sourceSha256": "88ba68dd66b6a0362d6c588cbf90151ba0c05522f3f118a6ec88d37e5075cfdb", + "sourceSha256": "e92a79f0c1757a77bcc1ce0dbc9e81a286aa194197f057e0ec65b251aefe38c9", "observations": [ "Repeated malformed inputs rejected and deep valid source resource-cancelled with successful next run" ], @@ -15878,10 +15878,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/LifecycleBoundaryContractTest.java", "suite": "guru.interlis.compiler.LifecycleBoundaryContractTest", "method": "syntaxNodeRecoveryAndTokenBudgetsRemainResourceCancellationRatherThanLanguageErrors", - "methodStart": 5870, - "methodEnd": 8263, - "methodSha256": "7a85bbdd07b68b1ca76a555a37902a55f90b052e77576e973d3d4d1dadee98bb", - "sourceSha256": "88ba68dd66b6a0362d6c588cbf90151ba0c05522f3f118a6ec88d37e5075cfdb", + "methodStart": 10700, + "methodEnd": 13367, + "methodSha256": "92795d8c3008d014049f8be4e3e293595ed040b8c46dcb70bafffc374a87744d", + "sourceSha256": "e92a79f0c1757a77bcc1ce0dbc9e81a286aa194197f057e0ec65b251aefe38c9", "observations": [ "Node/recovery/token limits and default fragment depth return explicit resource result" ], @@ -15898,10 +15898,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/LifecycleBoundaryContractTest.java", "suite": "guru.interlis.compiler.LifecycleBoundaryContractTest", "method": "cancellationAtEveryEmittedCompilerPhasePreservesInputsAndAllowsAFollowup", - "methodStart": 1048, - "methodEnd": 3043, - "methodSha256": "c853763c81f33566ac85b2d1a498a1e4085e96b27c5c7d0fb43ed1a830061e47", - "sourceSha256": "88ba68dd66b6a0362d6c588cbf90151ba0c05522f3f118a6ec88d37e5075cfdb", + "methodStart": 1442, + "methodEnd": 3987, + "methodSha256": "4f79afb42417a2b01c87018b20f931d505f744d18aa68c2863d948da2b17344e", + "sourceSha256": "e92a79f0c1757a77bcc1ce0dbc9e81a286aa194197f057e0ec65b251aefe38c9", "observations": [ "All four actually emitted compiler phases cancelled; one finish and successful fresh-equivalent followup" ], @@ -15919,10 +15919,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/LifecycleBoundaryContractTest.java", "suite": "guru.interlis.compiler.LifecycleBoundaryContractTest", "method": "internalFailuresAreTypedAndNeitherEventsNorSafeDiagnosticsExposeSecretInputs", - "methodStart": 4552, - "methodEnd": 5865, + "methodStart": 6139, + "methodEnd": 7452, "methodSha256": "18ac4151365d11ad21b0b92c7bad38ce91dc0677966c5d6c97ab08a7be142311", - "sourceSha256": "88ba68dd66b6a0362d6c588cbf90151ba0c05522f3f118a6ec88d37e5075cfdb", + "sourceSha256": "e92a79f0c1757a77bcc1ce0dbc9e81a286aa194197f057e0ec65b251aefe38c9", "observations": [ "Injected internal exception gives Failed/FailureId; marked secret absent from events and safe diagnostics" ], @@ -16066,10 +16066,10 @@ "source": "interlis-compiler/src/test/java/guru/interlis/compiler/EditorPartialStateContractTest.java", "suite": "guru.interlis.compiler.EditorPartialStateContractTest", "method": "missingAttributeTypeKeepsNeighborDefinitionCompletionAndExactPublicType", - "methodStart": 403, - "methodEnd": 2423, - "methodSha256": "5e03e406e5438177479276230df3089ae4d79b59265e361e4144931e0e1ee9b3", - "sourceSha256": "72c2711f113c2adaee9bb38ab05a551b2c3f2eebed8b39e2a34d744e64906d32", + "methodStart": 443, + "methodEnd": 2619, + "methodSha256": "cd9253175df30e61aa5992205fd910aa9e6e14ebf171fc9464b0a2c43cf62340", + "sourceSha256": "ab927ce857188a259e655b647c8be483ec81f19605b7662b55b725548dde2dea", "observations": [ "Broken attribute plus exact neighbor type, actual definition/completion and rejected compile" ], @@ -16261,11 +16261,11 @@ }, "PredefinedBindingContractTest.qualifiedTextAndBooleanDomainsUseCanonicalPredefinedBindingsInBothLanguages": { "source": "interlis-compiler/src/test/java/guru/interlis/compiler/PredefinedBindingContractTest.java", - "sourceSha256": "6f567643e5f74424a1a3ac842c18d97332fe42942924724ad16124df914cca67", + "sourceSha256": "d393008f1c2ad03e9a49e4e3728c3acd3a8d59e4055d65d06263337cc7b70af0", "suite": "guru.interlis.compiler.PredefinedBindingContractTest", "method": "qualifiedTextAndBooleanDomainsUseCanonicalPredefinedBindingsInBothLanguages", - "methodStart": 2043, - "methodEnd": 3984, + "methodStart": 2142, + "methodEnd": 4083, "methodSha256": "ca9e78369ddfc5ac2976705fdef0719f57c098c7ab88556290fce56eacd1d3a3", "observations": [ "Public compile gate accepts qualified INTERLIS.URI, INTERLIS.NAME and INTERLIS.BOOLEAN in both language versions.", @@ -16379,6 +16379,6 @@ "testdata/reference/norms/builtin-inventory.json": "be9dc5e65b20c15163ebaf42829961bb632105f88d57d6c2f4ead7175ba2451b", "testdata/verification/predefined-catalogue.json": "ea279974005f7dace7922099e8ea8ea499bb96ddcac9a9c5eeacb19721911e85", "testdata/compiler/character-sets/appendix-characters.properties": "c054fef59a89eb2b4434a217eea05b837a2f6e125a4b4fbf3cbf5af346dfa54e", - "docs/contracts/requirements.md": "fa64094305cd364535e9409582d229f206e27eab46b8c061e9973ec1f15661de" + "docs/contracts/requirements.md": "4a049a378365699cb3f0ba992fc245ca4c5ba079d3093c2067c3bd0a5f6bc954" } }