3

Is there a way to call require in a Lua file, and have the module set the environment of the file that calls it? For example, if I have a DSL (domain specific language) that defines the functions Root and Sequence defined in a table, can I have something like setfenv(1, dslEnv) in the module that allows me to access those functions like global variables?

The goal I in mind is using this is a behavior tree DSL in a way that makes my definition file look like this (or as close it as possible):

require "behaviortrees"

return Root {
    Sequence {
        Leaf "leafname",
        Leaf "leafname"
    }
}

without having to specifically bring Root, Sequence, and Leaf into scope explicitly or having to qualify names like behaviortrees.Sequence.

In short, I'm trying to make the definition file as clean as possible, without any extraneous lines cluttering the tree definition.

SashaZd
  • 3,315
  • 1
  • 26
  • 48
Azure Heights
  • 271
  • 2
  • 8
  • 1
    One of the points of `require` is that it isolates the requested module from the caller. If you want, you can copy any number of members from the returned table. Also, the `debug`-module contains functions breaking many of the normal rules. – Deduplicator Jul 31 '17 at 21:16
  • 1
    Can you give more details on what you're trying to achieve? – lhf Jul 31 '17 at 23:07
  • If you use these functions generally, then it is better to manually local each one, so at least all compilers can optimize them. –  Jul 31 '17 at 23:29

3 Answers3

2

At least in Lua 5.2, _ENV is a local that determinates the environment table. You can change the environment of any function, basically, the chunk.

_ENV = behaviortrees;

Another way is to automatically copy each field:

do
    _ENV = _ENV or _G;

    for k, v in next, behaviortrees do
        _ENV[k] = v;
    end
end

However it might be more efficient to manually local each field from behaviortrees.

  • 1
    About your comment in parentheses, a chunk is a function. – lhf Aug 01 '17 at 00:55
  • @lhf Thanks, I've clarified now. –  Aug 01 '17 at 00:57
  • 1
    @Hand1Cloud I didn't know you could change the environment of a do end chunk! That's pretty cool. Unfortunately though, I'm using Lua 5.1, which doesn't use the _ENV table. I didn't specify that explicitly in my post (there was a reference to setfenv, but that's pretty easy to miss), sorry about that. +1 still for teaching me something new! – Azure Heights Aug 01 '17 at 01:47
2

Can I have something like setfenv(1, dslEnv) in the module that allows me to access those functions like global variables?

Sure you can. You just have to figure out the correct stack level to use instead of the 1 in your setfenv call. Usually you'd walk up the stack using a loop with debug.getinfo calls until you find the require function on the stack, and then you move some more until you find the next main chunk (just in case someone calls require in a function). This is the stack level you'd have to use with setfenv. But may I suggest a ...

Different Approach

require in Lua is pluggable. You can add a function (called a searcher) to the package.loaders array, and require will call it when it tries to load a module. Let's suppose all your DSL files have a .bt suffix instead of the usual .lua. You'd then use a reimplementation of the normal Lua searcher with the differences that you'd look for .bt files instead of .lua files, and that you'd call setfenv on the function returned by loadfile. Something like this:

local function Root( x ) return x end
local function Sequence( x ) return x end
local function Leaf( x ) return x end


local delim = package.config:match( "^(.-)\n" ):gsub( "%%", "%%%%" )

local function searchpath( name, path )
  local pname = name:gsub( "%.", delim ):gsub( "%%", "%%%%" )
  local msg = {}
  for subpath in path:gmatch( "[^;]+" ) do
    local fpath = subpath:gsub( "%?", pname ):gsub("%.lua$", ".bt") -- replace suffix
    local f = io.open( fpath, "r" )
    if f then
      f:close()
      return fpath
    end
    msg[ #msg+1 ] = "\n\tno file '"..fpath.."'"
  end
  return nil, table.concat( msg )
end


local function bt_searcher( modname )
  assert( type( modname ) == "string" )
  local filename, msg = searchpath( modname, package.path )
  if not filename then
    return msg
  end
  local env = { -- create custom environment
    Root = Root,
    Sequence = Sequence,
    Leaf = Leaf,
  }
  local mod, msg = loadfile( filename )
  if not mod then
    error( "error loading module '"..modname.."' from file '"..filename..
           "':\n\t"..msg, 0 )
  end
  setfenv( mod, env ) -- set custom environment
  return mod, filename
end


table.insert( package.loaders, bt_searcher )

If you put this in a module and require it once from your main program, you can then require your DSL files with the custom environment from .bt files somewhere where you would put your .lua files as well. And you don't even need the require("behaviortrees") in your DSL files. E.g.:

File xxx.bt:

return Root {
  Sequence {
    Leaf "leafname",
    Leaf "leafname"
  }
}

File main.lua:

#!/usr/bin/lua5.1
require( "behaviortrees" ) -- loads the Lua module above and adds to package.loaders
print( require( "xxx" ) ) -- loads xxx.bt (but an xxx Lua module would still take precedence)
siffiejoe
  • 4,141
  • 1
  • 16
  • 16
  • So, user program (`xxx.bt`) written on DSL must be `require()`-ed instead of just executed as usual program (`dofile()`-ed)? – Egor Skriptunoff Aug 01 '17 at 08:18
  • @EgorSkriptunoff: Yes, `dofile` won't work. But I rarely use it anyway. Either the DSL file belongs to my program, in which case I use `require`, or it is user-supplied, in which case I use `loadfile` + `pcall`. In the latter case it is trivial to insert a `setfenv`. – siffiejoe Aug 02 '17 at 04:42
  • Why not use straightforward solution: redefine `dofile`? Internally it would do the same: check the file extension, `loadfile` + `setfenv` + `pcall`, but this would look more natural than using `require` for such unexpected purpose (loading non-modules). – Egor Skriptunoff Aug 02 '17 at 06:39
  • You are free to do that, I prefer not to redefine `dofile` -- especially if the semantics are different. `dofile` is `loadfile` + call, not `loadfile` + `pcall`. But of course `loadfile` + `setfenv` + `pcall` is a valid option. – siffiejoe Aug 02 '17 at 07:06
  • I mean `dofile` should be redefined only for `.bt` files, it should do its usual work for all the other files. `dofile` is used by most Lua people for EXECUTING a code, while `require` is used for caching the modules and prevent repetitive executions. When you are using `require` for usual executing - that's quite misleading. – Egor Skriptunoff Aug 02 '17 at 08:03
  • @EgorSkriptunoff: `dofile` just doesn't fit because it uses a non-protected call, and people expect it to do so. Overloading `dofile` for `.bt`-files to do a `pcall` is misleading, IMHO. If your DSL file is part of your application (and not user-supplied), I find `require` quite fitting, and you usually want the caching and path searching. The ability to load a non-Lua-based DSL dialect via a traditional parser in the same way, and to mock your DSL using a plain Lua module comes as a bonus. And if it is user-supplied, `lodfile` + `pcall` is clearer than a non-standard `dofile` overload. – siffiejoe Aug 05 '17 at 04:44
1

Module "behaviortrees.lua"

local behaviortrees = {
   -- insert your code for these functions
   Root     = function(...) ... end,
   Sequence = function(...) ... end,
   Leaf     = function(...) ... end,
}

-- Now set the environment of the caller.  Two ways are available:

-- If you want to make DSL environment isolated from Lua globals
-- (for example, "require" and "print" functions will not be available 
--  after executing require "behaviortrees")
setfenv(3, behaviortrees)
-- or 
-- If you want to preserve all globals for DSL
setfenv(3, setmetatable(behaviortrees, {__index = getfenv(3)}))

Main Lua program:

require "behaviortrees"

return Root {
   Sequence {
      Leaf "leafname",
      Leaf "leafname"
   }
}
Egor Skriptunoff
  • 906
  • 1
  • 8
  • 23