Lua Variables

Some notes on Lua variables.

Lua Variable Scope

Some variables are only accessible within only part of your code–such as within a certain file, or function, or block of code. We refer to this visible region for a variable as the “scope” of a variable. For example:

function test(x)
  print(y)                  -- (prints nil)
  local y = x^2 + 10
  local z = y^2             -- within scope of y
  print(y)                  -- within scope of y and z  (prints 14)
  print(z)                  -- within scope of y and z  (prints 196)
end
print(y)                    -- (prints nil)
test(2)
print(y)                    -- (prints nil)

The variable y can only be accessed on the three lines marked “within scope of y”, namely lines below the definition of the variable (local y statement) and within the function test. Any other attempt to access y will give nil (empty value).

This page describes some topics relating to scope.

Types of Variables

There are two main types of variables–local and global–with different scoping rules.

  • Global variables (which come into existence just by using them) are generally visible everywhere.
  • Local variables (which come into existence by declaring them with a local statement) are only visible to a block of code, meaning a file or part of a file.

In the above example, if we replace local y = x^2 + c with just y = x^2 + c , then y will be a global variable accessible from the final print statement, and even from other files.

The actual rules are if a local variable of the given name is not in scope, it will to access a global variable of that name, and if no global variable exists either, it will give nil.

When to User Global or Local Variables

It’s perfectly fine to always use global variables. But it’s recommended to use local whenever possible because local limits the visibility (scope) of a variable, which makes the program easier to understand or reason about.

Types of Global and Local Variables

It’s a little more than just “local or global”.

  • You can have local variables at the top-level of a file (visible to the entire file) or deep within some block of code:

    local x = 2   -- visible to all lines below it in the file
    function g()
      local y = 3  -- visible only in lines below it in the function
      print(x + y)
      if x > 0 then
        local z = 1  -- visible only in lines bloe it in the block
        print(x + z)
      end
      -- z not visible here
    end
    -- y not visible here
    
  • Global variables are stored in a table, like _G, so x = 5 may be equivalent to _G['x'] = 5. However, there can be multiple global tables called “environments” that might be something other than _G. SIMION workbench user programs run within its own environment, so your own codes doesn’t clobber anything really global to SIMION but goes away when you unload the workbench and load another workbench IOB. If you really want to write to a global variables that persists outside of your workbench, write to the _G table directly:

    simion.workbench_program()
    x = 5     -- global only visible in the workbench
    _G.y = 5  -- global that persists even if the workbench is closed
    

An Example

simion.workbench_program()
function segment.other_actions()
  local v = math.sqrt(ion_vx_mm^2 + ion_vy_mm^2 + ion_vz_mm^2)
  print('v is', v)
end
function segment.terminate()
  local v = math.sqrt(ion_vx_mm^2 + ion_vy_mm^2 + ion_vz_mm^2)
  print('v is', v)
end
function segment.terminate_run()
  print('v is', v)  -- this always prints nil.
end

The above defines two completely separate variables (two separate memory locations) that are both named v. The first is only visible in the other_actions segment. The second is only visible in the terminate segment. The terminate_run segment does not have access to either variable. The above code is actually equivalent to

simion.workbench_program()
function segment.other_actions()
  local v1 = math.sqrt(ion_vx_mm^2 + ion_vy_mm^2 + ion_vz_mm^2)
  print('v is', v1)
end
function segment.terminate()
  local v2 = math.sqrt(ion_vx_mm^2 + ion_vy_mm^2 + ion_vz_mm^2)
  print('v is', v2)
end
function segment.terminate_run()
  print('v is', v3)  -- this always prints nil.
end

Using Variables

My changes to adjustable variables are ignored.

Changes the user makes to adjustable variables on the Variables tab are only effective within segments.

Consider this example:

1:  simion.workbench_program()
2:  adjustable x = 1
3:  adjustable y = x
4:  print(x,y)
5:  function segment.initialize_run() print(x,y) end
6:

SIMION goes through these stages in processing the Lua code:

  • The Lua program is compiled and checked for syntax errors. Any adjustable variables (e.g. x) detected in compilation are added to the Variables tab, if they aren’t already. adjustable variables otherwise behave like Lua local variables.
  • The Lua file is executed from top to bottom. That Lua code is expected to do things like define Lua segments and the default values of adjustable variables.
    • Executing Line #1 tells SIMION it is a user program.
    • Executing Line #2 sets the default value of “x” to 1.
    • Executing Line #3 sets the default value of “y” to the then current value of x, which is 1.
    • Executing Line #4 prints “1 1”.
    • Executing Line #5 defines a segment, the content of which is not executed immediately.
  • After completing execution (Line #6), the then current values of the adjustable variables are used as the default values shown on the Variables tab, which the user may change.
  • SIMION re-assigns adjustable variables to values defined by the user on the Variables tab. For example, the user may set x to 10 and y to 11 from the Variables tab.
  • Finally, the Fly’m runs and calls the previously defined segments at appropriate stages of the Fly’m, which now print “10 11”.

If you want Line #4 to reflect user adjusted values on the Variables tab, you must move it inside a segment like in Line #5.

Testing the following workbench user program may make things clearer:

simion.workbench_program()
adjustable x = 1
print('A', x)  -- prints 1
x = 2
print('B', x)  -- prints 2
function segment.initialize()
  print('C', x)  -- prints 3 (or 4 on later executions)
  x = 4
  print('D', x)  -- prints 4
end
x = 3
print('E', x)  -- prints 3

The following example loads the HS1 collision model which defines an adjustable variable with pressure in terms of Pascal and instead defines it in terms of our own adjustable variable in units of Torr. Done properly, the original adjustable variable is set in the initialize_run segment.

simion.workbench_program()
local HS1 = simion.import 'collision_hs1.lua'
adjustable pressure_torr = 500
function segment.initialize_run()
  adjustable _pressure_pa = pressure_torr * (101325 / 760)
end

Merging Segments

A common scenario is if you define the same segment twice, such as when copying and pasting someone else’s code into your own program:

-- main.lua
function segment.initialize()
  print('one')
end
function segment.initialize()
  print('two')
end

That does not do what you might intend. The second segment definition overwrites the former one. So, the code is equivalent to this:

-- main.lua
function segment.initialize() print('two') end

You could merge the segments yourself:

-- main.lua
function segment.initialize()
  print('one')
  print('two')
end

Another option is to create two different functions that are themselves called by the single initialize segment:

-- main.lua
function initialize1() print('one') end
function initialize2() print('two') end
function segment.initialize()
  initialize1()
  initialize2()
end

or even save a copy of the old initialize segment before redefining it and calling the old one from the new one:

-- main.lua
function segment.initialize() print('one') end
local old_initialize = segment.initialize
function segment.initialize()
  old_initialize()
  print('two')
end

This need more typically occurs if you reuse Lua libraries that define segments in separate files and don’t want to touch the original files.

-- one.lua
function segment.initialize() print('two') end
-- two.lua
function segment.initialize() print('two') end
simion.workbench_program()
simion.import 'one.lua'
local one_initialize = segment.initialize
simion.import 'two.lua'
local two_initialize = segment.initialize
function segment.initialize()
  one_initialize()
  two_initialize()
end

Another approach used is

-- one.lua
local M = {segment = {}}
function M.segment.initialize() print('two') end
return M
-- two.lua
local M = {segment = {}}
function M.segment.initialize() print('two') end
return M
simion.workbench_program()
local ONE = simion.import 'one.lua'
local TWO = simion.import 'two.lua'
function segment.initialize()
  ONE.segment.initialize()
  TWO.segment.initialize()
end

You may even define a utility function to merge segment tables from libraries into the main segments:

local function merge_segments(t)
  for name,newseg in pairs(t) do
    local oldseg = segment[name]
    segment[name] =
      oldseg and function() oldseg(); newseg() end
             or  newseg
  end
end
simion.workbench_program()
local ONE = simion.import 'one.lua'
local TWO = simion.import 'two.lua'
merge_segments(ONE.segment)
merge_segments(TWO.segment)

References