]> git.lizzy.rs Git - metalua.git/blobdiff - metalua/treequery.mlua
Merge branch 'master' of ssh://git.eclipse.org/gitroot/koneki/org.eclipse.koneki...
[metalua.git] / metalua / treequery.mlua
diff --git a/metalua/treequery.mlua b/metalua/treequery.mlua
new file mode 100755 (executable)
index 0000000..f5b09d2
--- /dev/null
@@ -0,0 +1,467 @@
+-------------------------------------------------------------------------------
+-- Copyright (c) 2006-2013 Fabien Fleutot and others.
+--
+-- All rights reserved.
+--
+-- This program and the accompanying materials are made available
+-- under the terms of the Eclipse Public License v1.0 which
+-- accompanies this distribution, and is available at
+-- http://www.eclipse.org/legal/epl-v10.html
+--
+-- This program and the accompanying materials are also made available
+-- under the terms of the MIT public license which accompanies this
+-- distribution, and is available at http://www.lua.org/license.html
+--
+-- Contributors:
+--     Fabien Fleutot - API and implementation
+--
+-------------------------------------------------------------------------------
+
+local walk = require 'metalua.treequery.walk'
+
+local M = { }
+-- support for old-style modules
+treequery = M
+
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+--
+-- multimap helper mmap: associate a key to a set of values
+--
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+
+local function mmap_add (mmap, node, x)
+    if node==nil then return false end
+    local set = mmap[node]
+    if set then set[x] = true
+    else mmap[node] = {[x]=true} end
+end
+
+-- currently unused, I throw the whole set away
+local function mmap_remove (mmap, node, x)
+    local set = mmap[node]
+    if not set then return false
+    elseif not set[x] then return false
+    elseif next(set) then set[x]=nil
+    else mmap[node] = nil end
+    return true
+end
+
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+--
+-- TreeQuery object.
+--
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+
+local ACTIVE_SCOPE = setmetatable({ }, {__mode="k"})
+
+-- treequery metatable
+local Q = { }; Q.__index = Q
+
+--- treequery constructor
+--  the resultingg object will allow to filter ans operate on the AST
+--  @param root the AST to visit
+--  @return a treequery visitor instance
+function M.treequery(root)
+    return setmetatable({
+        root = root,
+        unsatisfied = 0,
+        predicates  = { },
+        until_up    = { },
+        from_up     = { },
+        up_f        = false,
+        down_f      = false,
+        filters     = { },
+    }, Q)
+end
+
+-- helper to share the implementations of positional filters
+local function add_pos_filter(self, position, inverted, inclusive, f, ...)
+    if type(f)=='string' then f = M.has_tag(f, ...) end
+    if not inverted then self.unsatisfied += 1 end
+    local x = {
+        pred      = f,
+        position  = position,
+        satisfied = false,
+        inverted  = inverted  or false,
+        inclusive = inclusive or false }
+    table.insert(self.predicates, x)
+    return self
+end
+
+function Q :if_unknown(f)
+    self.unknown_handler = f or (||nil)
+    return self
+end
+
+-- TODO: offer an API for inclusive pos_filters
+
+--- select nodes which are after one which satisfies predicate f
+Q.after     = |self, f, ...| add_pos_filter(self, 'after', false, false, f, ...)
+--- select nodes which are not after one which satisfies predicate f
+Q.not_after = |self, f, ...| add_pos_filter(self, 'after', true,  false, f, ...)
+--- select nodes which are under one which satisfies predicate f
+Q.under     = |self, f, ...| add_pos_filter(self, 'under', false, false, f, ...)
+--- select nodes which are not under one which satisfies predicate f
+Q.not_under = |self, f, ...| add_pos_filter(self, 'under', true,  false, f, ...)
+
+--- select nodes which satisfy predicate f
+function Q :filter(f, ...)
+    if type(f)=='string' then f = M.has_tag(f, ...) end
+    table.insert(self.filters, f);
+    return self
+end
+
+--- select nodes which satisfy predicate f
+function Q :filter_not(f, ...)
+    if type(f)=='string' then f = M.has_tag(f, ...) end
+    table.insert(self.filters, |...| not f(...))
+    return self
+end
+
+-- private helper: apply filters and execute up/down callbacks when applicable
+function Q :execute()
+    local cfg = { }
+    -- TODO: optimize away not_under & not_after by pruning the tree
+    function cfg.down(...)
+        --printf ("[down]\t%s\t%s", self.unsatisfied, table.tostring((...)))
+        ACTIVE_SCOPE[...] = cfg.scope
+        local satisfied = self.unsatisfied==0
+        for _, x in ipairs(self.predicates) do
+            if not x.satisfied and x.pred(...) then
+                x.satisfied = true
+                local node, parent = ...
+                local inc = x.inverted and 1 or -1
+                if x.position=='under' then
+                    -- satisfied from after we get down this node...
+                    self.unsatisfied += inc
+                    -- ...until before we get up this node
+                    mmap_add(self.until_up, node, x)
+                elseif x.position=='after' then
+                    -- satisfied from after we get up this node...
+                    mmap_add(self.from_up, node, x)
+                    -- ...until before we get up this node's parent
+                    mmap_add(self.until_up, parent, x)
+                elseif x.position=='under_or_after' then
+                    -- satisfied from after we get down this node...
+                    self.satisfied += inc
+                    -- ...until before we get up this node's parent...
+                    mmap_add(self.until_up, parent, x)
+                else
+                    error "position not understood"
+                end -- position
+                if x.inclusive then satisfied = self.unsatisfied==0 end
+            end -- predicate passed
+        end -- for predicates
+
+        if satisfied then
+            for _, f in ipairs(self.filters) do
+                if not f(...) then satisfied=false; break end
+            end
+            if satisfied and self.down_f then self.down_f(...) end
+        end
+    end
+
+    function cfg.up(...)
+        --printf ("[up]\t%s", table.tostring((...)))
+
+        -- Remove predicates which are due before we go up this node
+        local preds = self.until_up[...]
+        if preds then
+            for x, _ in pairs(preds) do
+                local inc = x.inverted and -1 or 1
+                self.unsatisfied += inc
+                x.satisfied = false
+            end
+            self.until_up[...] = nil
+        end
+
+        -- Execute the up callback
+        -- TODO: cache the filter passing result from the down callback
+        -- TODO: skip if there's no callback
+        local satisfied = self.unsatisfied==0
+        if satisfied then
+            for _, f in ipairs(self.filters) do
+                if not f(self, ...) then satisfied=false; break end
+            end
+            if satisfied and self.up_f then self.up_f(...) end
+        end
+
+        -- Set predicate which are due after we go up this node
+        local preds = self.from_up[...]
+        if preds then
+            for p, _ in pairs(preds) do
+                local inc = p.inverted and 1 or -1
+                self.unsatisfied += inc
+            end
+            self.from_up[...] = nil
+        end
+        ACTIVE_SCOPE[...] = nil
+    end
+
+    function cfg.binder(id_node, ...)
+        --printf(" >>> Binder called on %s, %s", table.tostring(id_node),
+        --      table.tostring{...}:sub(2,-2))
+        cfg.down(id_node, ...)
+        cfg.up(id_node, ...)
+        --printf("down/up on binder done")
+    end
+
+    cfg.unknown = self.unknown_handler
+
+    --function cfg.occurrence (binder, occ)
+    --   if binder then OCC2BIND[occ] = binder[1] end
+       --printf(" >>> %s is an occurrence of %s", occ[1], table.tostring(binder and binder[2]))
+    --end
+
+    --function cfg.binder(...) cfg.down(...); cfg.up(...) end
+    return walk.guess(cfg, self.root)
+end
+
+--- Execute a function on each selected node
+--  @down: function executed when we go down a node, i.e. before its children
+--         have been examined.
+--  @up: function executed when we go up a node, i.e. after its children
+--       have been examined.
+function Q :foreach(down, up)
+    if not up and not down then
+        error "iterator missing"
+    end
+    self.up_f = up
+    self.down_f = down
+    return self :execute()
+end
+
+--- Return the list of nodes selected by a given treequery.
+function Q :list()
+    local acc = { }
+    self :foreach(|x| table.insert(acc, x))
+    return acc
+end
+
+--- Return the first matching element
+--  TODO:  dirty hack, to implement properly with a 'break' return.
+--  Also, it won't behave correctly if a predicate causes an error,
+--  or if coroutines are involved.
+function Q :first()
+   local result = { }
+   local function f(...) result = {...}; error() end
+   pcall(|| self :foreach(f))
+   return unpack(result)
+end
+
+--- Pretty printer for queries
+function Q :__tostring() return "<treequery>" end
+
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+--
+-- Predicates.
+--
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+
+--- Return a predicate which is true if the tested node's tag is among the
+--  one listed as arguments
+-- @param ... a sequence of tag names
+function M.has_tag(...)
+    local args = {...}
+    if #args==1 then
+        local tag = ...
+        return (|node| node.tag==tag)
+        --return function(self, node) printf("node %s has_tag %s?", table.tostring(node), tag); return node.tag==tag end
+    else
+        local tags = { }
+        for _, tag in ipairs(args) do tags[tag]=true end
+        return function(node)
+            local node_tag = node.tag
+            return node_tag and tags[node_tag]
+        end
+    end
+end
+
+--- Predicate to test whether a node represents an expression.
+M.is_expr = M.has_tag('Nil', 'Dots', 'True', 'False', 'Number','String',
+                  'Function', 'Table', 'Op', 'Paren', 'Call', 'Invoke',
+                  'Id', 'Index')
+
+-- helper for is_stat
+local STAT_TAGS = { Do=1, Set=1, While=1, Repeat=1, If=1, Fornum=1,
+                    Forin=1, Local=1, Localrec=1, Return=1, Break=1 }
+
+--- Predicate to test whether a node represents a statement.
+--  It is context-aware, i.e. it recognizes `Call and `Invoke nodes
+--  used in a statement context as such.
+function M.is_stat(node, parent)
+    local tag = node.tag
+    if not tag then return false
+    elseif STAT_TAGS[tag] then return true
+    elseif tag=='Call' or tag=='Invoke' then return parent.tag==nil
+    else return false end
+end
+
+--- Predicate to test whether a node represents a statements block.
+function M.is_block(node) return node.tag==nil end
+
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+--
+-- Variables and scopes.
+--
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+
+local BINDER_PARENT_TAG = {
+   Local=true, Localrec=true, Forin=true, Function=true }
+
+--- Test whether a node is a binder. This is local predicate, although it
+--  might need to inspect the parent node.
+function M.is_binder(node, parent)
+   --printf('is_binder(%s, %s)', table.tostring(node), table.tostring(parent))
+   if node.tag ~= 'Id' or not parent then return false end
+   if parent.tag=='Fornum' then  return parent[1]==node end
+   if not BINDER_PARENT_TAG[parent.tag] then return false end
+   for _, binder in ipairs(parent[1]) do
+       if binder==node then return true end
+   end
+   return false
+end
+
+--- Retrieve the binder associated to an occurrence within root.
+--  @param occurrence an Id node representing an occurrence in `root`.
+--  @param root the tree in which `node` and its binder occur.
+--  @return the binder node, and its ancestors up to root if found.
+--  @return nil if node is global (or not an occurrence) in `root`.
+function M.binder(occurrence, root)
+    local cfg, id_name, result = { }, occurrence[1], { }
+    function cfg.occurrence(id)
+        if id == occurrence then result = cfg.scope :get(id_name) end
+        -- TODO: break the walker
+    end
+    walk.guess(cfg, root)
+    return unpack(result)
+end
+
+--- Predicate to filter occurrences of a given binder.
+--  Warning: it relies on internal scope book-keeping,
+--  and for this reason, it only works as query method argument.
+--  It won't work outside of a query.
+--  @param binder the binder whose occurrences must be kept by predicate
+--  @return a predicate
+
+-- function M.is_occurrence_of(binder)
+--     return function(node, ...)
+--         if node.tag ~= 'Id' then return nil end
+--         if M.is_binder(node, ...) then return nil end
+--         local scope = ACTIVE_SCOPE[node]
+--         if not scope then return nil end
+--         local result = scope :get (node[1]) or { }
+--         if result[1] ~= binder then return nil end
+--         return unpack(result)
+--     end
+-- end
+
+function M.is_occurrence_of(binder)
+    return function(node, ...)
+        local b = M.get_binder(node)
+        return b and b==binder
+    end
+end
+
+function M.get_binder(occurrence, ...)
+    if occurrence.tag ~= 'Id' then return nil end
+    if M.is_binder(occurrence, ...) then return nil end
+    local scope = ACTIVE_SCOPE[occurrence]
+    local binder_hierarchy = scope :get(occurrence[1])
+    return unpack (binder_hierarchy or { })
+end
+
+--- Transform a predicate on a node into a predicate on this node's
+--  parent. For instance if p tests whether a node has property P,
+--  then parent(p) tests whether this node's parent has property P.
+--  The ancestor level is precised with n, with 1 being the node itself,
+--  2 its parent, 3 its grand-parent etc.
+--  @param[optional] n the parent to examine, default=2
+--  @param pred the predicate to transform
+--  @return a predicate
+function M.parent(n, pred, ...)
+    if type(n)~='number' then n, pred = 2, n end
+    if type(pred)=='string' then pred = M.has_tag(pred, ...) end
+    return function(self, ...)
+        return select(n, ...) and pred(self, select(n, ...))
+    end
+end
+
+--- Transform a predicate on a node into a predicate on this node's
+--  n-th child.
+--  @param n the child's index number
+--  @param pred the predicate to transform
+--  @return a predicate
+function M.child(n, pred)
+    return function(node, ...)
+        local child = node[n]
+        return child and pred(child, node, ...)
+    end
+end
+
+--- Predicate to test the position of a node in its parent.
+--  The predicate succeeds if the node is the n-th child of its parent,
+--  and a <= n <= b.
+--  nth(a) is equivalent to nth(a, a).
+--  Negative indices are admitted, and count from the last child,
+--  as done for instance by string.sub().
+--
+--  TODO: This is wrong, this tests the table relationship rather than the
+--  AST node relationship.
+--  Must build a getindex helper, based on pattern matching, then build
+--  the predicate around it.
+--
+--  @param a lower bound
+--  @param a upper bound
+--  @return a predicate
+function M.is_nth(a, b)
+    b = b or a
+    return function(self, node, parent)
+        if not parent then return false end
+        local nchildren = #parent
+        local a = a<=0 and nchildren+a+1 or a
+        if a>nchildren then return false end
+        local b = b<=0 and nchildren+b+1 or b>nchildren and nchildren or b
+        for i=a,b do if parent[i]==node then return true end end
+        return false
+    end
+end
+
+
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+--
+-- Comments parsing.
+--
+-- -----------------------------------------------------------------------------
+-- -----------------------------------------------------------------------------
+
+local comment_extractor = |which_side| function (node)
+    local x = node.lineinfo
+    x = x and x[which_side]
+    x = x and x.comments
+    if not x then return nil end
+    local lines = { }
+    for _, record in ipairs(x) do
+        table.insert(lines, record[1])
+    end
+    return table.concat(lines, '\n')
+end
+
+M.comment_prefix = comment_extractor 'first'
+M.comment_suffix = comment_extractor 'last'
+
+
+--- Shortcut for the query constructor
+function M :__call(...) return self.treequery(...) end
+setmetatable(M, M)
+
+return M