Acquis 12 - AST

Lus can generate an abstract syntax tree from Lus code using the debug.parse function and command-line options, which is mostly useful for static analysis, code transformation, and tooling.

Programmatic Access

The debug.parse function parses a Lus source string and returns its AST as a nested table structure:

local ast = debug.parse("local x = 1 + 2")
-- Returns: {type = "chunk", line = 1, children = {...}}

Returns nil if parsing fails. An optional second argument specifies the chunk name for error messages:

local ast = debug.parse(code, "myfile.lus")

AST Structure

Each AST node is a table with:

  • type: Node type string (e.g., "chunk", "local", "binop")
  • line: Source line number where the node begins
  • Type-specific fields depending on the node type
  • children: Array of child statement nodes (for block-like nodes)

Example: Exploring an AST

local code = [[
local x = 1 + 2
print(x)
]]

local ast = debug.parse(code)

-- Walk the AST
local function walk(node, indent)
  indent = indent or 0
  local prefix = string.rep("  ", indent)
  print(prefix .. node.type .. " (line " .. node.line .. ")")

  if node.children then
    for _, child in ipairs(node.children) do
      walk(child, indent + 1)
    end
  end
end

walk(ast)
-- Output:
-- chunk (line 1)
--   local (line 1)
--   callstat (line 2)

Node Types

CategoryTypes
Statementschunk, block, local, global, assign, if, while, repeat, fornum, forgen, funcstat, localfunc, globalfunc, return, callstat, break, goto, label, catchstat, do
Expressionsnil, true, false, number, string, vararg, name, index, field, binop, unop, table, funcexpr, callexpr, methodcall, catchexpr, optchain, enum, from
Auxiliaryparam, namelist, explist, elseif, else, tablefield

Type-Specific Fields

Different node types have different fields:

-- Binary operation
{type = "binop", line = 1, op = "+", left = {...}, right = {...}}

-- Local declaration
{type = "local", line = 1, names = {...}, values = {...}}

-- Function call
{type = "callstat", line = 1, func = {...}, args = {...}}

-- If statement
{type = "if", line = 1, cond = {...}, children = {...}}

-- For loop (numeric)
{type = "fornum", line = 1, var = {...}, init = {...}, limit = {...}, step = {...}, children = {...}}

Command-Line Options

--ast-graph <file>

Parses a script and outputs its AST as a Graphviz DOT file for visualization:

lus --ast-graph output.dot script.lus
dot -Tpng output.dot -o ast.png

This does not execute the script, it only parses and outputs the AST structure.

--ast-json <file>

Parses a script and outputs its AST as a JSON file:

lus --ast-json output.json script.lus

Example JSON output:

{
  "type": "chunk",
  "line": 1,
  "children": [
    {
      "type": "local",
      "line": 1,
      "names": [{ "type": "name", "line": 1, "value": "x" }],
      "values": [
        {
          "type": "binop",
          "line": 1,
          "op": "+",
          "left": { "type": "number", "line": 1, "value": 1 },
          "right": { "type": "number", "line": 1, "value": 2 }
        }
      ]
    }
  ]
}

Use Cases

Static Analysis

-- Count function calls
local function count_calls(node)
  local count = 0
  if node.type == "callstat" or node.type == "callexpr" then
    count = 1
  end
  if node.children then
    for _, child in ipairs(node.children) do
      count = count + count_calls(child)
    end
  end
  return count
end

local ast = debug.parse(code)
print("Function calls:", count_calls(ast))

Code Metrics

-- Get all variable names
local function collect_names(node, names)
  names = names or {}
  if node.type == "name" then
    names[node.value] = true
  end
  if node.children then
    for _, child in ipairs(node.children) do
      collect_names(child, names)
    end
  end
  return names
end

Linting

-- Check for global assignments
local function check_globals(ast, filename)
  for _, child in ipairs(ast.children or {}) do
    if child.type == "assign" then
      for _, lhs in ipairs(child.lhs or {}) do
        if lhs.type == "name" then
          print(filename .. ":" .. child.line .. ": warning: global assignment to '" .. lhs.value .. "'")
        end
      end
    end
  end
end