Skip to content

Selection confinement

Suppose you're interested in a statement that appears only at a particular location in the program's structure, like a call to a function made at the module scope, where it will be evaluated as soon as the module itself is loaded. An "eager" one, if you will.

None of the queries we formed previously could regard the location of the target; they had constraints for type, or value, or contents, but never the location with respect to other statements.

(:at) and (:under) allow us to confine the selection to a particular scope. In this context, the term "scope" is used in the broad sense: it's not necessarily a lexical scope, but rather any statement that can contain another. Container might be a good alternative name to paint the picture clear.

Both selectors have the same signature of:

clojure
(:at [scope] [inner])
(:under [scope] [inner])

Selecting at an exact location

The first and largest scope would be the module itself, selectable with (mod):

clojure
(:at (mod) (call x))

Which would match calls to x made at exactly the module's scope:

javascript
x() // OK

function a() {
  x() // NOT ok
}

Were we interested in calls to x made only in the body of the function a:

clojure
(:at (fun a) (call x))

Which would conversely match the opposite of what was shown above:

javascript
x() // NOT ok

function a() {
  x() // OK

  function b() {
    x() // NOT ok
  }
}

Selecting at or below a location

(:under), in contrast to (:at), instead confines the selection to anywhere under the scope.

Given such a definition, we can finally construct the ultimate query in redundancy: (:under(mod) x)! All it's saying is "find me an identifier x anywhere inside the module", which is implied anyway. We could've just as well said x for exactly the same effect.

Joke aside, (:under) has the same signature as (:at) and can be used as such:

clojure
(:under (fun a)
        (call x))

Which would match as such:

javascript
x() // NOT ok

function a() {
  x() // OK

  function b() {
    x() // OK
  }
}

It might help to visualize the confinement, keeping in mind that (:at [x]) implies (:under [x]):

javascript
| // (:at mod)
| x()
|
| | function a() { // (:under (mod)) + (:at (fun a))
| |   x()
| |
| |   | function b() { // (:under (mod)) + (:under (fun a)) + (:at (fun b))
| |   |   x()
| |   | }
| |
| | }
|

But what exactly constitutes the boundary between (:at) and (:under)? How is it that once function b() {} was declared, all statements inside its body no longer qualify for (:at (fun a))?

Drawing confinement boundaries

WARNING

This is an advanced section and is certainly not required reading for the majority of use cases. If you find yourself needing more than what (:at) and (:under) provide, please file a feature request with your use case.

(:at) and (:under) are implemented in terms of a lower-level, generalized selector called (:lex) that has the following signature:

clojure
(:lex [distance] [boundary] [scope])

(:lex) is a predicate that is true only when the target is at most Nunits away from the scope, specified in distance, boundary and scope respectively. The boundary unit in the case of (:at) and (:under) is (fun) -- function declarations.

Using (:lex) as a primitive, we can define a selector that regards all lexical statements as a boundary:

clojure
(:def lexically-in
      (:lex 1 (:or (block)
                 (do-while)
                 (for)
                 (for-in)
                 (for-of)
                 (fun)
                 (if)
                 (switch)
                 (try)
                 (while)
                 (with)) \1))

In English: only when the target is no more than 1 units away from \1, where the distance is increased any time one of the following statements is encountered between the target and the scope:

javascript
{}
do {} while (...)
for (...) {}
for (x in y) { ... }
for (x of y) { ... }
function() {}
() => {}
if (...) {}
switch(...) { ... }
try { ... }
while (...) { ... }
with (...) { ... }

And in essence, this is a true (:at) that mirrors the lexical binding rules of the language. But in the context of SYNG, this is rarely useful; when you need to use confinement, you're far more likely to not want to consider all lexical statements, but rather only functions. And in case you do, a macro such as the above can take you there.

While (:lex) can be used on its own to find all statements within a certain scope, you'll most likely want to pair it with another selector to produce more meaningful results:

clojure
(:and (id x)
      (:lex 1 (fun) (fun a)))

Which is equivalent to the query (:at (fun a) x) that we've seen earlier. For reference, (:at) and (:under) are implemented in the following terms:

clojure
; (:at \1 \2)
(:and \2 (:lex 1 (fun) \1))

; (:under \1 \2)
(:and \2 (:lex _ (fun) \1))

Good luck!

Copyright © 2022-present Semantic Works, Inc.