ContainerLocalRewritePlanner.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.sandbox.jdt.container.analysis;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.sandbox.jdt.container.api.ContainerFlowComponent;
import org.sandbox.jdt.container.api.ContainerFlowGraph.ClosureStatus;
import org.sandbox.jdt.container.api.ContainerFlowGraph.FlowNode;
import org.sandbox.jdt.container.api.ContainerFlowGraph.NodeKind;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.ArgumentTransfer;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.DiagnosticKind;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.EditKind;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.LocalEdit;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.PlanningDiagnostic;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.PlanningResult;
import org.sandbox.jdt.container.api.ContainerMigrationReadiness;
import org.sandbox.jdt.container.api.ContainerMigrationReadiness.ExecutionStatus;
import org.sandbox.jdt.container.api.ContainerRecommendation;
import org.sandbox.jdt.container.api.ContainerShape;
import org.sandbox.jdt.container.api.ContainerUsageProfile;
import org.sandbox.jdt.container.api.ContainerUsageProfile.ElementDomain;
import org.sandbox.jdt.container.api.ContainerUsageProfile.OrderRequirement;
import org.sandbox.jdt.container.api.ContainerUsageProfile.ThreadExposure;
import org.sandbox.jdt.container.api.TargetContainerContract.Mutability;
import org.sandbox.jdt.container.api.UsageEvidence;
import org.sandbox.jdt.container.api.UsageEvidence.Kind;
/** Plans the first executable append-array to list migration. */
public final class ContainerLocalRewritePlanner {
private static final String LIST_TYPE= "java.util.List"; //$NON-NLS-1$
private static final String ARRAY_LIST_TYPE= "java.util.ArrayList"; //$NON-NLS-1$
private static final Set<Kind> SUPPORTED_EVIDENCE= EnumSet.of(
Kind.ARRAY_GROWTH,
Kind.APPEND_WRITE,
Kind.REFERENCE_COMPONENT,
Kind.ARRAY_LENGTH_READ,
Kind.ENCOUNTER_ITERATION,
Kind.FLOW_CONTINUATION_ROOT,
Kind.LOCAL_USAGE_COMPLETE);
/** Builds a local rewrite plan without cross-signature argument transfers. */
public PlanningResult plan(
ContainerFlowComponent component,
ContainerRecommendation recommendation,
ContainerMigrationReadiness readiness) {
return plan(component, recommendation, readiness, List.of());
}
/** Builds a local rewrite plan with exact aggregate argument targets. */
public PlanningResult plan(
ContainerFlowComponent component,
ContainerRecommendation recommendation,
ContainerMigrationReadiness readiness,
List<ArgumentTransfer> argumentTransfers) {
Objects.requireNonNull(component, "component"); //$NON-NLS-1$
Objects.requireNonNull(recommendation, "recommendation"); //$NON-NLS-1$
Objects.requireNonNull(readiness, "readiness"); //$NON-NLS-1$
argumentTransfers= List.copyOf(
Objects.requireNonNull(argumentTransfers, "argumentTransfers")); //$NON-NLS-1$
List<PlanningDiagnostic> diagnostics= new ArrayList<>();
if (readiness.status() != ExecutionStatus.AUTOMATIC) {
diagnostics.add(diagnostic(
DiagnosticKind.NOT_AUTOMATIC,
"The semantic execution gate has not approved automatic rewriting.")); //$NON-NLS-1$
}
validateTarget(recommendation, readiness, diagnostics);
FlowNode variable= strictlyLocalVariable(component, diagnostics);
ContainerUsageProfile profile= recommendation.sourceProfile();
if (variable != null && !variable.bindingKey().equals(profile.identity().bindingKey())) {
diagnostics.add(diagnostic(
DiagnosticKind.SOURCE_BINDING_MISMATCH,
"The recommendation binding does not match the closed local flow node.")); //$NON-NLS-1$
}
if (profile.elementDomain() != ElementDomain.REFERENCE
&& profile.elementDomain() != ElementDomain.ENUM) {
diagnostics.add(diagnostic(
DiagnosticKind.UNSUPPORTED_ELEMENT_DOMAIN,
"The first local rewrite supports only reference or enum array components.")); //$NON-NLS-1$
}
if (profile.access().hasPositionalSemantics()
|| profile.orderRequirement() == OrderRequirement.POSITIONAL) {
diagnostics.add(diagnostic(
DiagnosticKind.POSITIONAL_SEMANTICS,
"The first local rewrite does not yet translate indexed reads or positional writes.")); //$NON-NLS-1$
}
validateEvidence(profile, diagnostics);
validateArgumentTransfers(profile, argumentTransfers, diagnostics);
if (!diagnostics.isEmpty()) {
return PlanningResult.rejected(diagnostics);
}
return PlanningResult.accepted(new ContainerLocalRewritePlan(
variable.compilationUnitHandle(),
variable.bindingKey(),
LIST_TYPE,
ARRAY_LIST_TYPE,
recommendation.targetContract(),
edits(profile),
argumentTransfers));
}
private static void validateTarget(
ContainerRecommendation recommendation,
ContainerMigrationReadiness readiness,
List<PlanningDiagnostic> diagnostics) {
if (!recommendation.targetContract().equals(readiness.targetContract())
|| recommendation.targetContract().shape() != ContainerShape.LIST
|| recommendation.targetContract().mutability() != Mutability.MUTABLE
|| (recommendation.targetContract().orderRequirement()
!= OrderRequirement.ENCOUNTER
&& recommendation.targetContract().orderRequirement()
!= OrderRequirement.NONE)) {
diagnostics.add(diagnostic(
DiagnosticKind.UNSUPPORTED_TARGET,
"The first local rewrite supports a mutable list with encounter or unspecified order.")); //$NON-NLS-1$
}
}
private static FlowNode strictlyLocalVariable(
ContainerFlowComponent component,
List<PlanningDiagnostic> diagnostics) {
if (component.closureStatus() != ClosureStatus.LOCAL_CLOSED
|| component.nodes().size() != 1
|| !component.edges().isEmpty()
|| component.nodes().get(0).kind() != NodeKind.LOCAL_VARIABLE) {
diagnostics.add(diagnostic(
DiagnosticKind.FLOW_NOT_STRICTLY_LOCAL,
"The local member requires exactly one closed local variable and no local flow edges.")); //$NON-NLS-1$
return null;
}
FlowNode node= component.nodes().get(0);
if (node.compilationUnitHandle().isBlank() || node.bindingKey().isBlank()) {
diagnostics.add(diagnostic(
DiagnosticKind.FLOW_NOT_STRICTLY_LOCAL,
"The local flow node lacks a compilation-unit handle or binding key.")); //$NON-NLS-1$
return null;
}
return node;
}
private static void validateEvidence(
ContainerUsageProfile profile,
List<PlanningDiagnostic> diagnostics) {
long growthCount= count(profile, Kind.ARRAY_GROWTH);
long appendCount= count(profile, Kind.APPEND_WRITE);
if (growthCount == 0 || appendCount == 0) {
diagnostics.add(diagnostic(
DiagnosticKind.MISSING_APPEND_PATTERN,
"The profile does not contain a complete array growth and tail-write pattern.")); //$NON-NLS-1$
} else if (growthCount != appendCount) {
diagnostics.add(diagnostic(
DiagnosticKind.UNBALANCED_APPEND_PATTERN,
"Array growth and tail-write evidence counts differ.")); //$NON-NLS-1$
}
for (UsageEvidence evidence : profile.evidence()) {
if (!SUPPORTED_EVIDENCE.contains(evidence.kind())) {
diagnostics.add(diagnostic(
DiagnosticKind.UNSUPPORTED_EVIDENCE,
"The local profile contains unsupported evidence: "
+ evidence.kind() + '.')); //$NON-NLS-1$
}
}
}
private static void validateArgumentTransfers(
ContainerUsageProfile profile,
List<ArgumentTransfer> argumentTransfers,
List<PlanningDiagnostic> diagnostics) {
List<UsageEvidence> transferEvidence= profile.evidence().stream()
.filter(evidence -> evidence.kind() == Kind.FLOW_CONTINUATION_ROOT)
.toList();
Set<SourceRange> evidenceRanges= HashSet.newHashSet(transferEvidence.size());
for (UsageEvidence evidence : transferEvidence) {
evidenceRanges.add(new SourceRange(
evidence.sourceStart(), evidence.sourceLength()));
}
Set<SourceRange> describedRanges=
HashSet.newHashSet(argumentTransfers.size());
for (ArgumentTransfer transfer : argumentTransfers) {
describedRanges.add(new SourceRange(
transfer.sourceStart(), transfer.sourceLength()));
}
if (evidenceRanges.size() != transferEvidence.size()
|| describedRanges.size() != argumentTransfers.size()
|| !evidenceRanges.equals(describedRanges)) {
diagnostics.add(diagnostic(
DiagnosticKind.ARGUMENT_TRANSFER_MISMATCH,
"Every closed-flow argument occurrence requires one exact target method and parameter index.")); //$NON-NLS-1$
}
}
private static long count(ContainerUsageProfile profile, Kind kind) {
return profile.evidence().stream()
.filter(evidence -> evidence.kind() == kind)
.count();
}
private static List<LocalEdit> edits(ContainerUsageProfile profile) {
List<LocalEdit> result= new ArrayList<>();
result.add(new LocalEdit(
EditKind.CHANGE_LOCAL_DECLARATION,
profile.identity().sourceStart(),
profile.identity().sourceLength()));
result.add(new LocalEdit(
EditKind.REPLACE_EMPTY_ARRAY_INITIALIZER,
profile.identity().sourceStart(),
profile.identity().sourceLength()));
boolean verifyEncounterIterations=
profile.concurrency().exposure() == ThreadExposure.THREAD_CONFINED;
List<UsageEvidence> structuralAppendEvidence= profile.evidence().stream()
.filter(evidence -> evidence.kind() == Kind.ARRAY_GROWTH
|| evidence.kind() == Kind.APPEND_WRITE)
.toList();
for (UsageEvidence evidence : profile.evidence()) {
EditKind kind= switch (evidence.kind()) {
case ARRAY_GROWTH -> EditKind.REMOVE_ARRAY_GROWTH;
case APPEND_WRITE -> EditKind.REPLACE_TAIL_WRITE_WITH_ADD;
case ARRAY_LENGTH_READ -> isInsideAny(evidence, structuralAppendEvidence)
? null
: EditKind.REPLACE_LENGTH_WITH_SIZE;
case ENCOUNTER_ITERATION -> verifyEncounterIterations
? EditKind.VERIFY_ENCOUNTER_ITERATION
: null;
case FLOW_CONTINUATION_ROOT -> EditKind.VERIFY_ARGUMENT_TRANSFER;
default -> null;
};
if (kind != null) {
result.add(new LocalEdit(kind, evidence.sourceStart(), evidence.sourceLength()));
}
}
result.sort(Comparator
.comparingInt(LocalEdit::sourceStart)
.thenComparing(edit -> edit.kind().ordinal()));
return List.copyOf(result);
}
private static boolean isInsideAny(
UsageEvidence evidence,
List<UsageEvidence> containers) {
long start= evidence.sourceStart();
long end= start + evidence.sourceLength();
return containers.stream().anyMatch(container -> {
long containerStart= container.sourceStart();
long containerEnd= containerStart + container.sourceLength();
return start >= containerStart && end <= containerEnd;
});
}
private static PlanningDiagnostic diagnostic(
DiagnosticKind kind,
String message) {
return new PlanningDiagnostic(kind, message);
}
private record SourceRange(int start, int length) {
}
}