IrBGP.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.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
import org.eclipse.rdf4j.query.algebra.Var;
/**
* Textual IR for a WHERE/group block: ordered list of lines/nodes.
*
* Semantics: - Lines typically include triples ({@link IrStatementPattern} or {@link IrPathTriple}), modifiers
* ({@link IrFilter}, {@link IrBind}, {@link IrValues}), and container blocks such as {@link IrGraph},
* {@link IrOptional}, {@link IrMinus}, {@link IrUnion}, {@link IrService}. - Order matters: most transforms preserve
* relative order except where a local, safe rewrite explicitly requires adjacency. - Printing is delegated to
* {@link IrPrinter}; indentation and braces are handled there.
*/
public class IrBGP extends IrNode {
private static final boolean DEBUG_PROPERTY_LISTS = Boolean
.getBoolean("rdf4j.queryrender.debugPropertyLists");
private List<IrNode> lines = new ArrayList<>();
public IrBGP(boolean newScope) {
super(newScope);
}
public IrBGP(IrBGP where, boolean newScope) {
super(newScope);
add(where);
}
public IrBGP(List<IrNode> lines, boolean newScope) {
super(newScope);
this.lines = lines;
}
public List<IrNode> getLines() {
return lines;
}
public void add(IrNode node) {
if (node != null) {
lines.add(node);
}
}
@Override
public void print(IrPrinter p) {
p.openBlock();
if (isNewScope()) {
p.openBlock();
}
List<IrNode> ordered = stablyOrdered(lines);
printWithPropertyLists(p, ordered);
if (isNewScope()) {
p.closeBlock();
}
p.closeBlock();
}
@Override
public IrNode transformChildren(UnaryOperator<IrNode> op) {
IrBGP w = new IrBGP(this.isNewScope());
for (IrNode ln : this.lines) {
IrNode t = op.apply(ln);
t = t.transformChildren(op);
w.add(t == null ? ln : t);
}
return w;
}
@Override
public String toString() {
return "IrBGP{" +
"lines=" + Arrays.toString(lines.toArray()) +
'}';
}
private static List<IrNode> stablyOrdered(List<IrNode> in) {
if (in == null || in.size() < 2) {
return in;
}
// Heuristic: sort triples sharing anonymous bnode subjects so property-list intent is preserved.
boolean allTriples = in.stream().allMatch(n -> n instanceof IrStatementPattern);
if (!allTriples) {
return in;
}
boolean allAnonSubjects = in.stream().allMatch(n -> {
Var s = ((IrStatementPattern) n).getSubject();
return s != null && s.isAnonymous();
});
if (!allAnonSubjects) {
return in;
}
List<IrNode> copy = new ArrayList<>(in);
copy.sort((a, b) -> {
IrStatementPattern sa = (IrStatementPattern) a;
IrStatementPattern sb = (IrStatementPattern) b;
int c = name(sa.getSubject()).compareTo(name(sb.getSubject()));
if (c != 0) {
return c;
}
return name(sa.getPredicate()).compareTo(name(sb.getPredicate()));
});
return copy;
}
private static String name(Var v) {
return v == null ? "" : String.valueOf(v.getName());
}
private void printWithPropertyLists(IrPrinter p, List<IrNode> ordered) {
if (ordered == null || ordered.isEmpty()) {
return;
}
Map<String, List<IrStatementPattern>> bySubject = new LinkedHashMap<>();
Set<String> childSubjects = new HashSet<>();
for (IrNode n : ordered) {
if (n instanceof IrStatementPattern) {
IrStatementPattern sp = (IrStatementPattern) n;
if (isPropertyListCandidate(sp)) {
String subjName = name(sp.getSubject());
bySubject.computeIfAbsent(subjName, k -> new ArrayList<>()).add(sp);
Var obj = sp.getObject();
if (obj != null && obj.isAnonymous()) {
String objName = name(obj);
if (isAutoAnonBNodeName(objName)) {
childSubjects.add(objName);
}
}
}
}
}
if (DEBUG_PROPERTY_LISTS && !bySubject.isEmpty()) {
System.out.println("[irbgp-debug] property list subjects=" + bySubject.keySet()
+ " childSubjects=" + childSubjects);
}
for (IrNode n : ordered) {
if (n instanceof IrStatementPattern) {
IrStatementPattern sp = (IrStatementPattern) n;
if (isPropertyListCandidate(sp)) {
String subjName = name(sp.getSubject());
if (isAutoAnonBNodeName(subjName) && childSubjects.contains(subjName)
&& bySubject.containsKey(subjName)) {
if (DEBUG_PROPERTY_LISTS) {
System.out.println("[irbgp-debug] deferring nested property list for " + subjName);
}
continue;
}
if (bySubject.containsKey(subjName)) {
printPropertyList(subjName, bySubject, p);
}
continue;
}
}
if (n != null) {
n.print(p);
}
}
}
private void printPropertyList(String subjName, Map<String, List<IrStatementPattern>> bySubject, IrPrinter p) {
List<IrStatementPattern> props = bySubject.remove(subjName);
if (props == null || props.isEmpty()) {
return;
}
IrStatementPattern first = props.get(0);
String subjText = renderNodeOrVar(first.getSubjectOverride(), first.getSubject(), p);
String align = " ".repeat(Math.max(1, subjText.length() + 1));
for (int i = 0; i < props.size(); i++) {
IrStatementPattern sp = props.get(i);
StringBuilder sb = new StringBuilder();
if (i == 0) {
sb.append(subjText).append(" ");
} else {
sb.append(align);
}
sb.append(p.convertVarToString(sp.getPredicate())).append(" ");
sb.append(renderObject(sp, bySubject, p));
if (i == props.size() - 1) {
sb.append(" .");
} else {
sb.append(" ;");
}
p.line(sb.toString());
}
}
private String renderPropertyListInline(String subjName, Map<String, List<IrStatementPattern>> bySubject,
IrPrinter p) {
List<IrStatementPattern> props = bySubject.remove(subjName);
if (props == null || props.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < props.size(); i++) {
IrStatementPattern sp = props.get(i);
if (i > 0) {
sb.append(" ; ");
}
sb.append(p.convertVarToString(sp.getPredicate())).append(" ");
sb.append(renderObject(sp, bySubject, p));
}
return sb.toString();
}
private String renderObject(IrStatementPattern sp, Map<String, List<IrStatementPattern>> bySubject, IrPrinter p) {
if (sp.getObjectOverride() != null) {
StringBuilder tmp = new StringBuilder();
sp.getObjectOverride().print(new InlinePrinter(tmp, p::convertVarToString));
return tmp.toString();
}
Var obj = sp.getObject();
if (obj != null && obj.isAnonymous()) {
List<IrStatementPattern> nested = bySubject.get(name(obj));
if (nested != null && nested.size() >= 1) {
// inline nested property list
String nestedText = renderPropertyListInline(name(obj), bySubject, p);
return "[ " + nestedText + " ]";
}
}
return p.convertVarToString(obj);
}
private static String renderNodeOrVar(IrNode override, Var v, IrPrinter p) {
if (override != null) {
StringBuilder tmp = new StringBuilder();
override.print(new InlinePrinter(tmp, p::convertVarToString));
return tmp.toString();
}
if (v != null && v.isAnonymous()) {
String name = v.getName();
assert name == null || !name.startsWith("anon_");
if (name != null && name.startsWith("_anon_bnode_")) {
return "[]";
}
}
return p.convertVarToString(v);
}
private boolean isPropertyListCandidate(IrStatementPattern sp) {
if (sp == null || sp.getSubjectOverride() != null) {
return false;
}
Var s = sp.getSubject();
if (s == null || !s.isAnonymous()) {
return false;
}
String n = s.getName();
if (n == null) {
return false;
}
assert !n.startsWith("anon_");
if (n.startsWith("_anon_path_")) {
return false;
}
return n.startsWith("_anon_bnode_") || n.startsWith("_anon_user_bnode_");
}
private boolean isAutoAnonBNodeName(String n) {
if (n == null) {
return false;
}
assert !n.startsWith("anon_");
return n.startsWith("_anon_bnode_");
}
private static final class InlinePrinter implements IrPrinter {
private final StringBuilder out;
private final java.util.function.Function<Var, String> fmt;
InlinePrinter(StringBuilder out, java.util.function.Function<Var, String> fmt) {
this.out = out;
this.fmt = fmt;
}
@Override
public void startLine() {
}
@Override
public void append(String s) {
out.append(s);
}
@Override
public void endLine() {
}
@Override
public void line(String s) {
out.append(s);
}
@Override
public void openBlock() {
}
@Override
public void closeBlock() {
}
@Override
public void pushIndent() {
}
@Override
public void popIndent() {
}
@Override
public void printLines(List<IrNode> lines) {
if (lines == null) {
return;
}
for (IrNode n : lines) {
if (n != null) {
n.print(this);
}
}
}
@Override
public String convertVarToString(Var v) {
return fmt.apply(v);
}
}
@Override
public Set<Var> getVars() {
HashSet<Var> out = new HashSet<>();
for (IrNode ln : lines) {
if (ln != null) {
out.addAll(ln.getVars());
}
}
return out;
}
}