IrPathTriple.java

/*******************************************************************************
 * Copyright (c) 2025 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.queryrender.sparql.ir;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.queryrender.sparql.TupleExprIRRenderer;
import org.eclipse.rdf4j.queryrender.sparql.ir.util.transform.SimplifyPathParensTransform;
import org.eclipse.rdf4j.queryrender.sparql.util.VarUtils;

/**
 * Textual IR node for a property path triple: subject, path expression, object.
 *
 * Path expression is stored as pre-rendered text to allow local string-level rewrites (alternation/sequence grouping,
 * quantifiers) without needing a full AST here. Transforms are responsible for ensuring parentheses are added only when
 * required for correctness; printing strips redundant outermost parentheses for stable output.
 */
public class IrPathTriple extends IrTripleLike {

	private final String pathText;
	private Set<Var> pathVars; // vars that were part of the path before fusing (e.g., anon bridge vars)

	public IrPathTriple(Var subject, String pathText, Var object, boolean newScope, Set<Var> pathVars) {
		this(subject, null, pathText, object, null, pathVars, newScope);
	}

	public IrPathTriple(Var subject, IrNode subjectOverride, String pathText, Var object, IrNode objectOverride,
			Set<Var> pathVars, boolean newScope) {
		super(subject, subjectOverride, object, objectOverride, newScope);
		this.pathText = pathText;
		this.pathVars = Set.copyOf(pathVars);
	}

	public String getPathText() {
		return pathText;
	}

	@Override
	public String getPredicateOrPathText(TupleExprIRRenderer r) {
		return pathText;
	}

	/** Returns the set of variables that contributed to this path during fusing (e.g., anon _anon_path_* bridges). */
	public Set<Var> getPathVars() {
		return pathVars;
	}

	/** Assign the set of variables that contributed to this path during fusing. */
	public void setPathVars(Set<Var> vars) {
		if (vars.isEmpty()) {
			this.pathVars = Collections.emptySet();
		} else {
			this.pathVars = Set.copyOf(vars);
		}
	}

	/** Merge pathVars from 2+ IrPathTriples into a new unmodifiable set. */
	public static Set<Var> mergePathVars(IrPathTriple... pts) {
		if (pts == null || pts.length == 0) {
			return Collections.emptySet();
		}
		HashSet<Var> out = new HashSet<>();
		for (IrPathTriple pt : pts) {
			if (pt == null) {
				continue;
			}
			if (pt.getPathVars() != null) {
				out.addAll(pt.getPathVars());
			}
		}
		return out.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(out);
	}

	/**
	 * Create a set of pathVars from one or more IrStatementPattern by collecting any parser bridge variables
	 * (subject/object with names starting with _anon_path_ or _anon_path_inverse_) and anonymous predicate vars.
	 */
	public static Set<Var> fromStatementPatterns(IrStatementPattern... sps) {
		if (sps == null || sps.length == 0) {
			return Collections.emptySet();
		}
		HashSet<Var> out = new HashSet<>();
		for (IrStatementPattern sp : sps) {
			if (sp == null) {
				continue;
			}
			Var s = sp.getSubject();
			Var o = sp.getObject();
			Var p = sp.getPredicate();
			if (isAnonBridgeVar(s)) {
				out.add(s);
			}
			if (isAnonBridgeVar(o)) {
				out.add(o);
			}
			if (isAnonBridgeVar(p)) {
				out.add(p);
			}
		}
		return out.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(out);
	}

	private static boolean isAnonBridgeVar(Var v) {
		return VarUtils.isAnonPathVar(v) || VarUtils.isAnonPathInverseVar(v);
	}

	@Override
	public void print(IrPrinter p) {
		p.startLine();
		if (getSubjectOverride() != null) {
			getSubjectOverride().print(p);
		} else {
			p.append(p.convertVarToString(getSubject()));
		}
		// Apply lightweight string-level path simplification at print time for stability/readability
		String simplified = SimplifyPathParensTransform.simplify(pathText);
		p.append(" " + simplified + " ");

		if (getObjectOverride() != null) {
			getObjectOverride().print(p);
		} else {
			p.append(p.convertVarToString(getObject()));
		}

		p.append(" .");
		p.endLine();
	}

	@Override
	public String toString() {
		return "IrPathTriple{" +
				"pathText='" + pathText + '\'' +
				", pathVars=" + Arrays.toString(pathVars.toArray()) +
				", subject=" + subject +
				", subjectOverride=" + subjectOverride +
				", object=" + object +
				", objectOverride=" + objectOverride +
				'}';
	}

	@Override
	public Set<Var> getVars() {
		HashSet<Var> out = new HashSet<>(super.getVars());
		if (pathVars != null) {
			out.addAll(pathVars);
		}
		return out;
	}
}