@@ -60,6 +60,8 @@ const FUNS = { // SPARQL builtin -> a3 call name (manifesty.yaml map)
6060const AGGS = new Set ( [ "count" , "sum" , "min" , "max" , "avg" , "sample" , "group_concat" ] ) ;
6161
6262function expr ( e , ctx ) {
63+ if ( e . termType === "Variable" && ctx ?. rename && ! ctx . inScope . has ( "?" + e . value ) )
64+ return ctx . fresh ( e . value ) ; // rule 2: out-of-scope -> renamed apart
6365 if ( e . termType != null ) return term ( e ) ;
6466 if ( e . type === "aggregate" ) {
6567 const name = e . aggregation . toLowerCase ( ) ;
@@ -117,62 +119,98 @@ function bgp(triples, ind) {
117119 return lines . join ( " .\n" + ind ) ;
118120}
119121
120- /** compile a sparqljs pattern ARRAY (one group's contents) */
121- function group ( patterns , ind ) {
122+ /** wrap a group's compilation in scope(...) share(its in-scope vars) */
123+ function scoped ( patterns , ind , ctx ) {
124+ const V = [ ...inScope ( patterns ) ] ;
125+ const inner = { ...ctx , inScope : inScope ( patterns , new Set ( ctx . inScope ) ) } ;
126+ return "scope ( " + group ( patterns , ind + " " , inner ) + " )"
127+ + " share (" + V . join ( " " ) + ")" ;
128+ }
129+
130+ /** compile a sparqljs pattern ARRAY (one group's contents).
131+ * ctx = {mode:"topdown"|"bottomup", inScope:Set, fresh, rename:bool} */
132+ function group ( patterns , ind , ctx = { mode : "topdown" } ) {
133+ const bu = ctx . mode === "bottomup" ;
134+ const ectx = bu ? { ...ctx , rename : true ,
135+ inScope : inScope ( patterns , new Set ( ctx . inScope ) ) } : undefined ;
136+ const filters = [ ] ; // bottom-up: group filters apply after all joins
122137 const parts = [ ] ; // each a conjunct string
123138 for ( const p of patterns ) {
124139 switch ( p . type ) {
125140 case "bgp" :
126141 if ( p . triples . length ) parts . push ( bgp ( p . triples , ind ) ) ;
127142 break ;
128143 case "optional" :
129- parts . push ( "~( " + group ( p . patterns , ind + " " ) + " )" ) ;
144+ if ( bu ) {
145+ /* rule 3: absorb the optional group's IMMEDIATE filters - they sit
146+ * outside the scope, at the LeftJoin, where outer names stay visible */
147+ const direct = p . patterns . filter ( x => x . type === "filter"
148+ && x . expression ?. operator ?. toLowerCase ( ) !== "notexists" ) ;
149+ const rest = p . patterns . filter ( x => ! direct . includes ( x ) ) ;
150+ const joinScope = inScope ( rest , new Set ( ectx . inScope ) ) ;
151+ const jctx = { ...ctx , rename : true , inScope : joinScope } ;
152+ let o = scoped ( rest , ind + " " , ctx ) ;
153+ for ( const f of direct )
154+ o += " .\n" + ind + " {" + expr ( f . expression , jctx ) + "}" ;
155+ parts . push ( direct . length ? "~( " + o + " )" : "~ " + o ) ;
156+ } else
157+ parts . push ( "~( " + group ( p . patterns , ind + " " , ctx ) + " )" ) ;
130158 break ;
131159 case "minus" : {
132160 /* MINUS binds to the group built so far: (acc |! (P)) */
133161 const acc = parts . length ? parts . join ( " .\n" + ind ) : "" ;
134- const rhs = "( " + group ( p . patterns , ind + " " ) + " )" ;
162+ const rhs = bu ? "( " + scoped ( p . patterns , ind + " " , ctx ) + " )"
163+ : "( " + group ( p . patterns , ind + " " , ctx ) + " )" ;
135164 parts . length = 0 ;
136165 parts . push ( acc ? "( " + acc + "\n" + ind + " |! " + rhs + " )" : "|! " + rhs ) ;
137166 break ;
138167 }
139168 case "union" :
140- parts . push ( "( " + p . patterns . map ( b =>
141- group ( b . type === "group" ? b . patterns : [ b ] , ind + " " ) ) . join ( "\n" + ind + "|&\n" + ind + " " ) + " )" ) ;
169+ parts . push ( "( " + p . patterns . map ( b => {
170+ const ps = b . type === "group" ? b . patterns : [ b ] ;
171+ return bu ? scoped ( ps , ind + " " , ctx ) : group ( ps , ind + " " , ctx ) ;
172+ } ) . join ( "\n" + ind + "|&\n" + ind + " " ) + " )" ) ;
142173 break ;
143174 case "group" :
144- parts . push ( "( " + group ( p . patterns , ind + " " ) + " )" ) ;
175+ parts . push ( bu ? scoped ( p . patterns , ind + " " , ctx )
176+ : "( " + group ( p . patterns , ind + " " , ctx ) + " )" ) ;
145177 break ;
146178 case "graph" :
147- parts . push ( "in " + term ( p . name ) + " ( " + group ( p . patterns , ind + " " ) + " )" ) ;
179+ parts . push ( "in " + term ( p . name ) + " ( "
180+ + ( bu ? scoped ( p . patterns , ind + " " , ctx ) : group ( p . patterns , ind + " " , ctx ) ) + " )" ) ;
148181 break ;
149182 case "filter" : {
150183 const op = p . expression ?. operator ?. toLowerCase ( ) ;
151184 if ( op === "notexists" ) {
152185 const arg = p . expression . args [ 0 ] ;
153- parts . push ( "!( " + group ( arg . patterns ?? [ arg ] , ind + " " ) + " )" ) ;
186+ /* NB in bottom-up mode ! evaluates as Minus; SPARQL NOT EXISTS's
187+ * substitution semantics are the top-down reading */
188+ parts . push ( "!( " + group ( arg . patterns ?? [ arg ] , ind + " " , ctx ) + " )" ) ;
154189 }
155190 else if ( op === "exists" )
156191 throw new Error ( "unsupported: FILTER EXISTS (use the pattern directly)" ) ;
192+ else if ( bu )
193+ filters . push ( "{" + expr ( p . expression , ectx ) + "}" ) ; // group filters join last
157194 else
158195 parts . push ( "{" + expr ( p . expression ) + "}" ) ;
159196 break ;
160197 }
161198 case "bind" :
162- parts . push ( "let (" + term ( p . variable ) + " " + expr ( p . expression ) + ")" ) ;
199+ parts . push ( "let (" + term ( p . variable ) + " " + expr ( p . expression , ectx ) + ")" ) ;
163200 break ;
164201 case "values" :
165202 parts . push ( valuesBlock ( p . values , ind ) ) ;
166203 break ;
167204 case "query" :
168- parts . push ( subselect ( p , ind ) ) ;
205+ parts . push ( subselect ( p , ind , ctx ) ) ;
169206 break ;
170207 case "service" :
171208 throw new Error ( "unsupported: SERVICE" ) ;
172209 default :
173210 throw new Error ( "unsupported pattern: " + p . type ) ;
174211 }
175212 }
213+ parts . push ( ...filters ) ;
176214 return parts . join ( " .\n" + ind ) ;
177215}
178216
@@ -183,6 +221,58 @@ function valuesBlock(values, ind) {
183221 return "bindings (" + vars . join ( " " ) + ") { " + rows . join ( " " ) + " }" ;
184222}
185223
224+ /* ── section 4 (bottom-up) machinery ────────────────────────────────────── */
225+
226+ /** SPARQL in-scope variables of a pattern list (18.2.1-ish) */
227+ function inScope ( patterns , into = new Set ( ) ) {
228+ for ( const p of patterns ?? [ ] ) {
229+ switch ( p . type ) {
230+ case "bgp" :
231+ for ( const t of p . triples )
232+ for ( const k of [ "subject" , "predicate" , "object" ] )
233+ if ( t [ k ] . termType === "Variable" ) into . add ( "?" + t [ k ] . value ) ;
234+ break ;
235+ case "bind" : into . add ( term ( p . variable ) ) ; break ;
236+ case "values" : for ( const v of Object . keys ( p . values [ 0 ] ?? { } ) ) into . add ( v ) ; break ;
237+ case "graph" :
238+ if ( p . name . termType === "Variable" ) into . add ( term ( p . name ) ) ;
239+ inScope ( p . patterns , into ) ; break ;
240+ case "optional" : case "group" : case "union" :
241+ inScope ( p . patterns , into ) ; break ;
242+ case "query" :
243+ for ( const v of p . variables ?? [ ] )
244+ if ( v . termType === "Variable" ) into . add ( term ( v ) ) ;
245+ else if ( v . variable ) into . add ( term ( v . variable ) ) ;
246+ break ;
247+ case "minus" : case "filter" : break ; // not in-scope contributors
248+ }
249+ }
250+ return into ;
251+ }
252+
253+ /** fresh-name allocator over the whole query's variables (?v -> ?v_1) */
254+ function freshener ( allVars ) {
255+ const made = new Map ( ) ;
256+ return name => {
257+ if ( made . has ( name ) ) return made . get ( name ) ;
258+ let n = 1 ;
259+ while ( allVars . has ( "?" + name + "_" + n ) ) ++ n ;
260+ const f = "?" + name + "_" + n ;
261+ allVars . add ( f ) ;
262+ made . set ( name , f ) ;
263+ return f ;
264+ } ;
265+ }
266+
267+ function allQueryVars ( node , into = new Set ( ) ) {
268+ if ( ! node || typeof node !== "object" ) return into ;
269+ if ( node . termType === "Variable" ) into . add ( "?" + node . value ) ;
270+ for ( const v of Object . values ( node ) )
271+ if ( Array . isArray ( v ) ) v . forEach ( x => allQueryVars ( x , into ) ) ;
272+ else if ( v && typeof v === "object" ) allQueryVars ( v , into ) ;
273+ return into ;
274+ }
275+
186276/* projections + solution modifiers shared by SELECT and subselect */
187277function collectClause ( q , ind ) {
188278 let proj ;
@@ -211,14 +301,15 @@ function collectClause(q, ind) {
211301}
212302
213303/** a nested SELECT becomes a scoped sub-pipeline sharing its projection */
214- function subselect ( q , ind ) {
304+ function subselect ( q , ind , ctx = { mode : "topdown" } ) {
215305 if ( q . queryType !== "SELECT" ) throw new Error ( "unsupported nested query: " + q . queryType ) ;
216306 const inner = ind + " " ;
217307 const shared = q . variables . some ( v => v . termType === "Wildcard" )
218308 ? [ ...collectVars ( q . where ) ]
219309 : q . variables . map ( v => v . termType === "Variable" ? term ( v ) : term ( v . variable ) ) ;
220310 const { collect, having } = collectClause ( q , inner ) ;
221- let body = "ask ( " + group ( q . where , inner ) + " )\n" + inner + collect ;
311+ const sctx = { ...ctx , inScope : new Set ( ) } ; // subselect isolates
312+ let body = "ask ( " + group ( q . where , inner , sctx ) + " )\n" + inner + collect ;
222313 for ( const h of having ) body += "\n" + inner + h ;
223314 return "scope ( " + body + "\n" + ind + " ) share (" + shared . join ( " " ) + ")" ;
224315}
@@ -243,10 +334,21 @@ function collectVars(patterns, into = new Set()) {
243334
244335/* ── whole queries ──────────────────────────────────────────────────────── */
245336
246- export function compile ( ast ) {
337+ export function compile ( ast , opts = { } ) {
338+ const mode = opts . mode === "bottomup" ? "bottomup" : "topdown" ;
339+ const ctx = { mode, inScope : new Set ( ) ,
340+ fresh : freshener ( allQueryVars ( ast ) ) } ;
247341 const out = [ ] ;
248- out . push ( "# compiled from SPARQL (sparql-to-algae3.md section 3, top-down)" ) ;
249- out . push ( "require <http://www.w3.org/ns/algae3#eval-topdown>" ) ;
342+ if ( mode === "bottomup" ) {
343+ out . push ( "# compiled from SPARQL (sparql-to-algae3.md section 4, bottom-up:" ) ;
344+ out . push ( "# groups scoped to their in-scope variables; out-of-scope" ) ;
345+ out . push ( "# expression variables renamed apart; OPTIONAL-immediate" ) ;
346+ out . push ( "# filters absorbed at the LeftJoin)" ) ;
347+ out . push ( "require <http://www.w3.org/ns/algae3#eval-bottomup>" ) ;
348+ } else {
349+ out . push ( "# compiled from SPARQL (sparql-to-algae3.md section 3, top-down)" ) ;
350+ out . push ( "require <http://www.w3.org/ns/algae3#eval-topdown>" ) ;
351+ }
250352 for ( const [ pfx , iri ] of Object . entries ( ast . prefixes ?? { } ) )
251353 out . push ( "prefix " + pfx + ": <" + iri + ">" ) ;
252354 if ( ast . base ) out . push ( "base <" + ast . base + ">" ) ;
@@ -258,16 +360,16 @@ export function compile(ast) {
258360 switch ( ast . queryType ) {
259361 case "SELECT" : {
260362 const { collect, having } = collectClause ( ast , "" ) ;
261- out . push ( "ask ( " + group ( ast . where , ind ) + " )" ) ;
363+ out . push ( "ask ( " + group ( ast . where , ind , ctx ) + " )" ) ;
262364 out . push ( collect ) ;
263365 for ( const h of having ) out . push ( h ) ;
264366 break ;
265367 }
266368 case "ASK" :
267- out . push ( "test ( " + group ( ast . where , ind ) + " )" ) ;
369+ out . push ( "test ( " + group ( ast . where , ind , ctx ) + " )" ) ;
268370 break ;
269371 case "CONSTRUCT" :
270- out . push ( "ask ( " + group ( ast . where , ind ) + " )" ) ;
372+ out . push ( "ask ( " + group ( ast . where , ind , ctx ) + " )" ) ;
271373 out . push ( "assert ( " + bgp ( ast . template ?? [ ] , ind ) + " )" ) ;
272374 break ;
273375 default :
@@ -277,7 +379,7 @@ export function compile(ast) {
277379}
278380
279381/** parse text with a sparqljs Parser instance and compile */
280- export function compileText ( sparqlText , SparqlParser , baseIRI ) {
382+ export function compileText ( sparqlText , SparqlParser , baseIRI , opts ) {
281383 const parser = new SparqlParser ( baseIRI ? { baseIRI } : { } ) ;
282- return compile ( parser . parse ( sparqlText ) ) ;
384+ return compile ( parser . parse ( sparqlText ) , opts ?? { } ) ;
283385}
0 commit comments