blob: 52eabe5d3f0fff2567bb0c23868c197d42657d3e [file] [log] [blame]
-------------------------------------------------------------------------------
-- 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
--
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
--
-- This library walks AST to gather information about the identifiers
-- in it. It classifies them between free variables and bound
-- variables, and keeps track of which AST node created a given bound
-- variable occurence.
--
-- walk_id (kind, ast)
--
-- Input:
-- * an AST kind: 'expr', 'stat', 'block', 'expr_list', 'binder_list', 'guess'
-- * an AST of the corresponding kind.
--
-- > string, AST
--
-- Output: a table with two fields, 'bound' and 'free';
-- * free associates the name of each free variable with the list of
-- all its occurences in the AST. That list is never empty.
-- * bound associates each stat or expr binding a new variable with
-- the occurences of that/those new variable(s).
--
-- > { free = table (string, AST and `Id{ });
-- > bound = table (AST, table(AST and `Id{ })) }
--
-- How it works
-- ============
-- Walk the tree to:
-- * locate open variables, and keep pointers on them so that they can
-- be alpha converted.
-- * locate variable bindings, so that we can find bound variables
-- * locate bound variables, keep them in association with their
-- binder, again in order to alpha-convert them.
--
-- Special treatments:
-- * `Function `Local `Localrec `Fornum `Forin have binders;
-- `Local takes effect from the next statement,
-- `Localrec from the current statement,
-- `Function and other statments inside their bodies.
-- * `Repeat has a special scoping rule for its condition.
-- * blocks create temporary scopes
-- * `Splice must stop the walking, so that user code won't be
-- converted
--
-------------------------------------------------------------------------------
-{ extension ('match', ...) }
-- -{ extension ('log', ...) }
require 'metalua.walk'
require 'metalua.walk.scope'
-- variable lists auto-create empty list as values by default.
local varlist_mt = { __index = function (self, key)
local x={ }; self[key] = x; return x
end }
local function _walk_id (kind, supercfg, ast, ...)
assert(walk[kind], "Inbalid AST kind selector")
assert(type(supercfg=='table'), "Config table expected")
assert(type(ast)=='table', "AST expected")
local cfg = { expr = { }; block = { }; stat = { } }
local scope = scope:new()
local visit_bound_var, visit_free_var
if not supercfg.id then
printf("Warning, you're using the id walker without id visitor. "..
"If you know what you want do to, then you're probably doing "..
"something else...")
visit_bound_var = || nil
visit_free_var = || nil
else
visit_free_var = supercfg.id.free or || nil
visit_bound_var = supercfg.id.bound or || nil
end
-----------------------------------------------------------------------------
-- Check identifiers; add functions parameters to scope
-----------------------------------------------------------------------------
function cfg.expr.down(x, ...)
-- Execute the generic expression walker; if it breaks.
-- don't do the id walking.
if supercfg.expr and supercfg.expr.down then
local r = supercfg.expr.down(x, ...)
if r then return r end
end
local parents = {...}
match x with
| `Id{ name } ->
local binder, r = scope.current[name] -- binder :: ast which bound var
if binder then
--$log( 'walk.id found a bound var:', x, binder)
r = visit_bound_var(x, binder, unpack(parents))
else
--$log( 'walk.id found a free var:', x, scope.current)
r = visit_free_var(x, unpack(parents))
end
if r then return r end
| `Function{ params, _ } -> scope:push (params, x)
| `Stat{ block, expr } ->
-------------------------------------------------------------
-- 'expr' is in the scope of 'block': create the scope and
-- walk the block 'manually', then prevent automatic walk
-- by returning 'break'.
-------------------------------------------------------------
scope:push()
for _, stat in ipairs (block) do walk.stat(cfg, stat, x, ...) end
walk.expr(cfg, expr, x, unpack(parents))
scope:pop()
return 'break'
| _ -> -- pass
end
end
-----------------------------------------------------------------------------
-- Close the function scope opened by 'down()'
-----------------------------------------------------------------------------
function cfg.expr.up(x, ...)
match x with `Function{...} -> scope:pop() | _ -> end
if supercfg.expr and supercfg.expr.up then supercfg.expr.up(x, ...) end
end
-----------------------------------------------------------------------------
-- Create a new scope and register loop variable[s] in it
-----------------------------------------------------------------------------
function cfg.stat.down(x, ...)
-- Execute the generic statement walker; if it breaks.
-- don't do the id walking.
if supercfg.stat and supercfg.stat.down then
local r = supercfg.stat.down(x, ...)
if r then return r end
end
match x with
| `Forin{ vars, ... } -> scope:push (vars, x)
| `Fornum{ var, ... } -> scope:push ({var}, x)
| `Localrec{ vars, ... } -> scope:add (vars, x)
| `Repeat{ block, expr } ->
-------------------------------------------------------------
-- 'expr' is in the scope of 'block': create the scope and
-- walk the block 'manually', then prevent automatic walk
-- by returning 'break'.
-------------------------------------------------------------
scope:push()
for _, stat in ipairs (block) do walk.stat(cfg, stat, x, ...) end
walk.expr(cfg, expr, x, ...)
scope:pop()
return 'break'
| _ -> -- pass
end
end
-----------------------------------------------------------------------------
-- Close the scopes opened by 'up()'
-----------------------------------------------------------------------------
function cfg.stat.up(x, ...)
match x with
| `Forin{ ... } | `Fornum{ ... } -> scope:pop()
| `Local{ vars, ... } -> scope:add(vars, x)
| _ -> -- pass
-- `Repeat has no up(), because it 'break's.
end
if supercfg.stat and supercfg.stat.up then supercfg.stat.up(x, ...) end
end
-----------------------------------------------------------------------------
-- Create a separate scope for each block
-----------------------------------------------------------------------------
function cfg.block.down(x, ...)
if supercfg.block and supercfg.block.down then
local r = supercfg.block.down(x, ...)
if r then return r end
end
scope:push()
end
function cfg.block.up(x, ...)
scope:pop()
if supercfg.block and supercfg.block.up then supercfg.block.up(x, ...) end
end
cfg.binder = supercfg.binder
walk[kind](cfg, ast, ...)
end
local mt = { __index = |_,k| |...| _walk_id(k, ...) }
walk_id = setmetatable({ }, mt)