Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ tasks.named("compileJava") {

// Ensure sources JAR includes generated sources and depends on code generation
tasks.named<Jar>("sourcesJar") {
dependsOn("generateGrammarSource", "javaccSparqlCorese")
dependsOn("generateGrammarSource", "generateTestGrammarSource", "javaccSparqlCorese")
from(javaccGeneratedDir)
from(antlrPackageDir)
includeEmptyDirs = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import fr.inria.corese.core.next.query.impl.parser.semantic.support.VariableScopeAnalyzer;
import fr.inria.corese.core.next.query.impl.sparql.ast.*;
import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*;
import fr.inria.corese.core.next.query.impl.sparql.ast.path.*;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;

Expand Down Expand Up @@ -56,6 +57,11 @@ public abstract class SparqlAstBuilder {
*/
protected final Deque<List<TriplePatternAst>> bgpStack = new ArrayDeque<>();

/**
* Counter for anonymous blank nodes created while expanding {@code [ ... ]} and {@code ( ... )} subjects.
*/
private int anonymousBlankNodeCounter;

/**
* Stack of currently open SELECT operations (top-level SELECT and nested SELECT subqueries).
*/
Expand Down Expand Up @@ -363,19 +369,24 @@ protected SelectFrame getCurrentSelectFrame() {
}

/**
* Add a triple pattern (?s ?p ?o) to the current BGP (TriplesBlock).
* Add a triple pattern to the current BGP (TriplesBlock).
* This must be called while inside a TriplesBlock.
*/
public void addTriple(TermAst s, TermAst p, TermAst o) {
public void addTriple(TermAst s, PathAst p, TermAst o) {
if (bgpStack.isEmpty()) {
// This should not happen if listener wiring is correct, but we fail loudly
// because BGP boundaries matter (TriplesBlock).
throw new IllegalStateException("addTriple() called outside of TriplesBlock (BGP). " +
"Ensure you call enterBgp() on enterTriplesBlock.");
}
bgpStack.peek().add(new TriplePatternAst(s, p, o));
}

/**
* Add a triple whose predicate is a single term (IRI, variable, etc.).
*/
public void addTriple(TermAst s, TermAst p, TermAst o) {
addTriple(s, PathAst.from(p), o);
}

// --- Filters ---

/**
Expand Down Expand Up @@ -1278,12 +1289,221 @@ public TermAst termFromReplace(SparqlParser.StrReplaceExpressionContext ctx) {
}

/**
* Predicate as a property path.
* For simple triples without a composed path, this is just an iriRef or 'a'.
* Composed property paths will be expanded in a later phase if needed.
* Predicate as a SPARQL 1.1 property path.
*/
public TermAst termFromVerbPath(SparqlParser.VerbPathContext ctx) {
return this.iri(ctx.getText());
public PathAst pathFromVerbPath(SparqlParser.VerbPathContext ctx) {
return pathFromPath(ctx.path());
}

public PathAst pathFromPath(SparqlParser.PathContext ctx) {
return pathFromPathAlternative(ctx.pathAlternative());
}

private PathAst pathFromPathAlternative(SparqlParser.PathAlternativeContext ctx) {
return foldAlternatives(ctx.pathSequence().stream().map(this::pathFromPathSequence).toList());
}

private PathAst pathFromPathSequence(SparqlParser.PathSequenceContext ctx) {
return foldSequences(ctx.pathEltOrInverse().stream().map(this::pathFromPathEltOrInverse).toList());
}

private PathAst pathFromPathEltOrInverse(SparqlParser.PathEltOrInverseContext ctx) {
if (ctx.CARET() != null) {
return new InversePathAst(pathFromPathElt(ctx.pathElt()));
}
return pathFromPathElt(ctx.pathElt());
}

private PathAst pathFromPathElt(SparqlParser.PathEltContext ctx) {
PathAst primary = pathFromPathPrimary(ctx.pathPrimary());
SparqlParser.PathModContext mod = ctx.pathMod();
if (mod == null) {
return primary;
}
if (mod.QUESTION() != null) {
return new OptionalPathAst(primary);
}
if (mod.STAR() != null) {
return new ZeroOrMorePathAst(primary);
}
if (mod.PLUS() != null) {
return new OneOrMorePathAst(primary);
}
throw new QueryEvaluationException("Unexpected path modifier in " + ctx.getText());
}

private PathAst pathFromPathPrimary(SparqlParser.PathPrimaryContext ctx) {
if (ctx.iriRef() != null) {
return PathAst.from(termFromIriRef(ctx.iriRef()));
}
if (ctx.A() != null) {
return PathAst.from(iri("a"));
}
if (ctx.EXCLAMATION() != null) {
return pathFromPathNegatedPropertySet(ctx.pathNegatedPropertySet());
}
if (ctx.path() != null) {
return pathFromPath(ctx.path());
}
throw new QueryEvaluationException("Unexpected path primary in " + ctx.getText());
}

private PathAst pathFromPathNegatedPropertySet(SparqlParser.PathNegatedPropertySetContext ctx) {
List<PathAst> excluded;
if (ctx.L_PAREN() != null) {
excluded = ctx.pathOneInPropertySet().stream().map(this::pathFromPathOneInPropertySet).toList();
} else {
excluded = List.of(pathFromPathOneInPropertySet(ctx.pathOneInPropertySet().getFirst()));
}
return new NegatedPropertySetPathAst(excluded);
}

private PathAst pathFromPathOneInPropertySet(SparqlParser.PathOneInPropertySetContext ctx) {
if (ctx.CARET() != null) {
if (ctx.iriRef() != null) {
return new InversePathAst(PathAst.from(termFromIriRef(ctx.iriRef())));
}
return new InversePathAst(PathAst.from(iri("a")));
}
if (ctx.iriRef() != null) {
return PathAst.from(termFromIriRef(ctx.iriRef()));
}
return PathAst.from(iri("a"));
}

private PathAst foldAlternatives(List<PathAst> parts) {
if (parts.isEmpty()) {
throw new QueryEvaluationException("Empty property path alternative");
}
PathAst result = parts.getFirst();
for (int i = 1; i < parts.size(); i++) {
result = new AlternativePathAst(result, parts.get(i));
}
return result;
}

private PathAst foldSequences(List<PathAst> parts) {
if (parts.isEmpty()) {
throw new QueryEvaluationException("Empty property path sequence");
}
PathAst result = parts.getFirst();
for (int i = 1; i < parts.size(); i++) {
result = new SequencePathAst(result, parts.get(i));
}
return result;
}

/**
* Creates a fresh anonymous blank node label for expanded BGP triples.
*/
public TermAst newAnonymousBlankNode() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generates fresh blank nodes with labels like _:b0. That can collide with user-written blank node labels and make two different nodes become equal in the AST.

@Test
void generatedBlankNodeShouldNotCollideWithUserBlankNodeLabel() {
    SparqlParser parser = newParserDefault();

    QueryAst ast = parser.parse("""
            PREFIX ex: <http://example.org/>
            SELECT * WHERE {
              _:b0 ex:link [ ex:p ?o ] .
            }
            """);

    BgpAst bgp = whereBgp(ast);
    assertEquals(2, bgp.triples().size());

    TriplePatternAst inner = bgp.triples().getFirst();
    TriplePatternAst outer = bgp.triples().get(1);

    assertInstanceOf(IriAst.class, outer.subject());
    assertInstanceOf(IriAst.class, outer.object());
    assertNotEquals(outer.subject(), outer.object());
    assertNotEquals(outer.subject(), inner.subject());
}

return iri("_:b" + anonymousBlankNodeCounter++);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use fr.inria.corese.core.next.data.impl.io.common.IOConstants#BLANK_NODE_PREFIX in case we want to config it later

}

/**
* Expands a {@code propertyListPathNotEmpty} into triple patterns for the given subject.
*/
public void addTriplesFromPropertyListPath(
TermAst subject,
SparqlParser.PropertyListPathNotEmptyContext propertyList) {
var verbPaths = propertyList.verbPath();
var verbSimples = propertyList.verbSimple();
var objectLists = propertyList.objectListPath();

int verbPathIdx = 0;
int verbSimpleIdx = 0;

for (SparqlParser.ObjectListPathContext objectList : objectLists) {
PathAst predicate = predicateFromPropertyListPath(
verbPaths, verbSimples, verbPathIdx, verbSimpleIdx);
if (verbPathIdx < verbPaths.size()
&& (verbSimpleIdx >= verbSimples.size()
|| verbPaths.get(verbPathIdx).getStart().getTokenIndex()
< verbSimples.get(verbSimpleIdx).getStart().getTokenIndex())) {
verbPathIdx++;
} else {
verbSimpleIdx++;
}

for (TermAst object : termListFromObjectListPath(objectList)) {
addTriple(subject, predicate, object);
}
}
}

private PathAst predicateFromPropertyListPath(
List<SparqlParser.VerbPathContext> verbPaths,
List<SparqlParser.VerbSimpleContext> verbSimples,
int verbPathIdx,
int verbSimpleIdx) {
boolean useVerbPath = verbPathIdx < verbPaths.size()
&& (verbSimpleIdx >= verbSimples.size()
|| verbPaths.get(verbPathIdx).getStart().getTokenIndex()
< verbSimples.get(verbSimpleIdx).getStart().getTokenIndex());
if (useVerbPath) {
return pathFromVerbPath(verbPaths.get(verbPathIdx));
}
return PathAst.from(termFromVerbSimple(verbSimples.get(verbSimpleIdx)));
}

/**
* Builds triple patterns for a {@code triplesNodePath} subject and returns its head term.
*/
public TermAst subjectFromTriplesNodePath(SparqlParser.TriplesNodePathContext ctx) {
if (ctx.blankNodePropertyListPath() != null) {
return subjectFromBlankNodePropertyListPath(ctx.blankNodePropertyListPath());
}
if (ctx.collectionPath() != null) {
return subjectFromCollectionPath(ctx.collectionPath());
}
throw new QueryEvaluationException("Unexpected triples node path: " + ctx.getText());
}

private TermAst subjectFromBlankNodePropertyListPath(
SparqlParser.BlankNodePropertyListPathContext ctx) {
TermAst blankNode = newAnonymousBlankNode();
if (ctx.propertyListPathNotEmpty() != null) {
addTriplesFromPropertyListPath(blankNode, ctx.propertyListPathNotEmpty());
}
return blankNode;
}

private TermAst subjectFromCollectionPath(SparqlParser.CollectionPathContext ctx) {
List<SparqlParser.GraphNodePathContext> nodes = ctx.graphNodePath();
if (nodes.isEmpty()) {
throw new QueryEvaluationException("Empty RDF collection in triple pattern");
}

TermAst head = newAnonymousBlankNode();
TermAst current = head;
for (int i = 0; i < nodes.size(); i++) {
addTriple(current, rdfTerm(RDF.first), termFromGraphNodePath(nodes.get(i)));
if (i == nodes.size() - 1) {
addTriple(current, rdfTerm(RDF.rest), rdfTerm(RDF.nil));
} else {
TermAst next = newAnonymousBlankNode();
addTriple(current, rdfTerm(RDF.rest), next);
current = next;
}
}
return head;
}

/**
* Graph node inside a property-path triple (variable/term, blank node property list, or collection).
*/
public TermAst termFromGraphNodePath(SparqlParser.GraphNodePathContext ctx) {
if (ctx.varOrTerm() != null) {
return termFromVarOrTerm(ctx.varOrTerm());
}
if (ctx.triplesNodePath() != null) {
return subjectFromTriplesNodePath(ctx.triplesNodePath());
}
throw new QueryEvaluationException("Unexpected graph node path: " + ctx.getText());
}

private TermAst rdfTerm(RDF term) {
return new IriAst("<" + term.getIRI().stringValue() + ">");
}

/**
Expand All @@ -1300,12 +1520,7 @@ public TermAst termFromVerbSimple(SparqlParser.VerbSimpleContext ctx) {
public List<TermAst> termListFromObjectListPath(SparqlParser.ObjectListPathContext ctx) {
List<TermAst> out = new ArrayList<>();
for (var objPath : ctx.objectPath()) {
var graphNodePath = objPath.graphNodePath();
if (graphNodePath.varOrTerm() != null) {
out.add(termFromVarOrTerm(graphNodePath.varOrTerm()));
} else {
out.add(this.iri(graphNodePath.getText()));
}
out.add(termFromGraphNodePath(objPath.graphNodePath()));
}
return out;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public void exitConstructTemplate() {
* Adds a triple to the CONSTRUCT template (inside {@code ConstructTriples}, not WHERE).
*/
public void addConstructTriple(TermAst s, TermAst p, TermAst o) {
constructTriples.add(new TriplePatternAst(s, p, o));
constructTriples.add(TriplePatternAst.of(s, p, o));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,36 +84,22 @@ public void exitTriplesSameSubject(SparqlParser.TriplesSameSubjectContext ctx) {

@Override
public void exitTriplesSameSubjectPath(SparqlParser.TriplesSameSubjectPathContext ctx) {
TermAst s = builder().termFromVarOrTerm(ctx.varOrTerm());
var pl = ctx.propertyListPathNotEmpty();
if (pl == null) return;

var verbPaths = pl.verbPath();
var verbSimples = pl.verbSimple();
var objLists = pl.objectListPath();

int verbPathIdx = 0;
int verbSimpleIdx = 0;

for (SparqlParser.ObjectListPathContext objList : objLists) {
TermAst p;

boolean useVerbPath = verbPathIdx < verbPaths.size() && (
verbSimpleIdx >= verbSimples.size() ||
verbPaths.get(verbPathIdx).getStart().getTokenIndex()
< verbSimples.get(verbSimpleIdx).getStart().getTokenIndex()
);

if (useVerbPath) {
p = builder().termFromVerbPath(verbPaths.get(verbPathIdx++));
} else {
p = builder().termFromVerbSimple(verbSimples.get(verbSimpleIdx++));
}

List<TermAst> objects = builder().termListFromObjectListPath(objList);
for (TermAst o : objects) {
builder().addTriple(s, p, o);
SparqlAstBuilder b = builder();
if (ctx.varOrTerm() != null) {
var propertyList = ctx.propertyListPathNotEmpty();
if (propertyList == null) {
return;
}
b.addTriplesFromPropertyListPath(b.termFromVarOrTerm(ctx.varOrTerm()), propertyList);
return;
}
if (ctx.triplesNodePath() == null) {
return;
}
TermAst subject = b.subjectFromTriplesNodePath(ctx.triplesNodePath());
SparqlParser.PropertyListPathContext propertyListPath = ctx.propertyListPath();
if (propertyListPath != null && propertyListPath.propertyListPathNotEmpty() != null) {
b.addTriplesFromPropertyListPath(subject, propertyListPath.propertyListPathNotEmpty());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.inria.corese.core.next.query.impl.parser.semantic.support;

import fr.inria.corese.core.next.query.impl.sparql.ast.*;
import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst;

/**
* Default implementation for AstVisitor
Expand Down Expand Up @@ -101,4 +102,9 @@ public void visit(ServiceAst ast) {
public void visit(GraphRefAst ast) {

}

@Override
public void visit(PathAst ast) {

}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.inria.corese.core.next.query.impl.parser.semantic.support;

import fr.inria.corese.core.next.query.impl.sparql.ast.*;
import fr.inria.corese.core.next.query.impl.sparql.ast.path.PathAst;

/**
* Visitor interface for the AST hierarchy
Expand All @@ -25,4 +26,5 @@ public interface AstVisitor {
void visit(PrefixDeclarationAst ast);
void visit(ServiceAst ast);
void visit(GraphRefAst ast);
void visit(PathAst ast);
}
Loading
Loading