Introduction to Lua

Although I heard of Lua several years ago, I decided to study this language recently when Lua entered the top 10 languages of the TIOBE Programming Community Index. To my surprise, Lua is almost an ideal dynamic typed functional language, because it is simple, efficient and powerful. As a learning practice, I wrote a program to draw a graph of any equation or inequation with two variables in Lua. If you are new to Lua, I strongly recommend you read the book Programming in Lua and visit lua.org.

Lua is an extensible extension language. Extensible means libraries can be written in C and accessed in Lua in natural ways. Extension means it can be embedded into a host application so that users can program it. However due to the following two reasons, I want to write a library in C# and call the library in Lua:

  1. I don't like to program in C/C++
  2. None of the Lua libraries can match the functionality provided by the .NET Framework

I didn't find an existing solution so I implemented an Lua interpreter by myself. Since the interpreter is written in C#, .NET library can be called in Lua code when it is appropriately wrapped in modules.

Implementation of the Interpreter

The syntax of Lua is defined with parser expression grammar in Lua.Grammar.txt file. Then given the grammar file as input, a homemade parser generator is used to generate the parser code. If the lua code is parsed successfully, a syntax tree with Chunk as root node is returned. Then the interpreter executes the Chunk according to Lua semantics.

Most of the implementation is straightforward, a little difference with standard Lua is that strings are unicode and the library function string.format uses the same formatter syntax as in C#'s string.Format.

The project code is compiled into two files: lua.exe and wlua.exe, one is command line version and the other is winform version. Here is the result after running the test.lua file:

LuaInterpreter/luatest.png

Windows Forms Library

As a proof of the concept, I write a module to create a UI using Windows Forms. After reading the code in WinFormLib.cs, you will understand why Lua is a miraculous language, the metatable mechanism is more powerful than firstly imagined. The module is named as "Gui", the same .NET type, method, property name can be used to manipulate controls. Here is an example program in WinFormExample.wlua:

form = Gui.Form{
    Text="Main Form", Height=200, StartPosition="CenterScreen",
    Gui.Label{ Text="Hello!", Name="lable", Width=80, Height=17, Top=9, Left=12 },
    Gui.Button{ Text="Click", Width=80, Height=23, Top=30, Left=12,
        Click=function(sender,e) Gui.ShowMessage(lable.Text,"Clicked") end },
}

Gui.Run(form)

Gui.ControlTypeName returns a lua function to create an instance of the control, the function accepts a lua table as its parameter, key value pairs in the table are used to set values to control properties, array items in the table are added as child controls or sub items of the control. One special thing is that when set the Name property, a global variable is created for the control. As you see, Lua as a data descriptive language is more compact than an equivalent XAML file.

The screen shot of this example is:

LuaInterpreter/Example.png

The Ledger.wlua file contains a more complete and practical example, it can add and delete entries to a ledger sheet and save to a file, the saved file can be opened later. Here is the code:

form = Gui.Form{
    Text="Ledger Sheet", Width=700, Height=500, StartPosition="CenterScreen",
    Gui.SplitContainer {
        Dock="Fill", Width=700, SplitterDistance=200,
        Gui.TreeView{ Name="treeviewCategory", Dock="Fill", HideSelection=false },
        Gui.Panel{
            Dock="Fill",
            Gui.ListView{ 
                Name="listviewEntries", Dock="Fill", View="Details", 
			GridLines=true, FullRowSelect=true,
                ContextMenuStrip=Gui.ContextMenuStrip {
                    Gui.ToolStripMenuItem { Text="Delete", 
			Click=function(sender,e) DeleteEntry() end }
                }
            },
            Gui.StatusStrip { Name="statusStrip", Dock="Bottom" }
        }
    },
    Gui.ToolStrip{
        Dock="Top", Top=0, Left=0, Width=700, Height=25,
        Gui.ToolStripButton { Name="btnOpen", Text="&Open", 
				Width=88, Height=22, Image="icon\\open.png" },
        Gui.ToolStripButton { Name="btnSave", Text="&Save", 
				Width=88, Height=22, Image="icon\\save.png" },
        Gui.ToolStripButton { Name="btnAdd", Text="&Add", 
				Width=88, Height=22, Image="icon\\add.png" },
    }
}

incomeNode = treeviewCategory.Nodes.Add("Income")
outgoNode = treeviewCategory.Nodes.Add("Outgo")
listviewEntries.Columns.Add(Gui.ColumnHeader{ Text="Date", Width=100 })
listviewEntries.Columns.Add(Gui.ColumnHeader{ Text="Detail", Width=260 })
listviewEntries.Columns.Add(Gui.ColumnHeader{ Text="Amount", Width=120 })

dialog = Gui.Form{
    Text="Add Entry", Width=320, Height=220, StartPosition="CenterParent", 
		FormBorderStyle="FixedDialog", ShowInTaskbar = false;
    Gui.Label{ Text="Subject:", Width=60, Height=17, Top=14, Left=12 },
    Gui.RadioButton { Name="dialog_Income", Text="Income", 
				Width=80, Height=20, Top=9, Left=80 },
    Gui.RadioButton { Name="dialog_Outgo", Text="Outgo", 
				Width=80, Height=20, Top=9, Left=160, Checked=true },
    Gui.Label{ Text="Category:", Width=60, Height=17, Top=40, Left=12 },
    Gui.ComboBox { Name="dialog_Category", Width=160, Height=20, Top=36, Left=80 },
    Gui.Label{ Text="Detail:", Width=60, Height=17, Top=68, Left=12 },
    Gui.TextBox { Name="dialog_Detail", Width=160, Height=20, Top=64, Left=80 },
    Gui.Label{ Text="Amount:", Width=60, Height=17, Top=96, Left=12 },
    Gui.TextBox { Name="dialog_Amount", Width=128, Height=20, Top=92, Left=80 },
    Gui.Label{ Text="Date:", Width=60, Height=17, Top=128, Left=12 },
    Gui.DateTimePicker { Name="dialog_Date", Width=128, 
				Height=21, Top=124, Left=80, Format="Short" },
    Gui.Button{ Text="OK", Name="dialog_btnOK", Width=80, 
				Height=23, Top=156, Left=130, DialogResult="OK" },
    Gui.Button{ Text="Cancel", Name="dialog_btnCancel", Width=80, 
				Height=23, Top=156, Left=224, DialogResult="Cancel" },
    AcceptButton=dialog_btnOK,
    CancelButton=dialog_btnCancel
}

Entries = {}

btnAdd.Click = function (sender,e)
    dialog_Detail.Text = ""
    dialog_Amount.Text = ""
    if treeviewCategory.SelectedNode ~= nil and 
			treeviewCategory.SelectedNode.Tag ~= nil then
        dialog_Category.Text = treeviewCategory.SelectedNode.Text
    end
    if dialog.ShowDialog(form) == "OK" then
        local subject = dialog_Income.Checked and "income" or "outgo"
        local category = dialog_Category.Text
        local detail = dialog_Detail.Text
        local amount = dialog_Amount.Text
        local date = dialog_Date.Value.ToShortDateString()
        local entry = {date, subject, category, detail, amount}
        table.insert(Entries, entry)
        local categoryNode = UpdateCategoryTree(entry)
        if treeviewCategory.SelectedNode == categoryNode then
            AddEntryToListView(entry)
        else
            treeviewCategory.SelectedNode = categoryNode
        end
    end
end

function FindCategoryNode(entry)
    local subject = entry[2]
    local category = entry[3]
    local subjectNode = subject == "outgo" and outgoNode or incomeNode
    local subNodes = subjectNode.Nodes.Find(category, false)
    if #subNodes == 0 then
        return nil, subjectNode
    else
        return subNodes[1], subjectNode
    end
end

function UpdateCategoryTree(entry)
    local categoryNode, subjectNode = FindCategoryNode(entry)
    if categoryNode == nil then
        local category = entry[3]
        categoryNode = subjectNode.Nodes.Add(category, category)
        categoryNode.Tag = {}
        dialog_Category.Items.Add(category)
    end
    table.insert(categoryNode.Tag, entry)
    return categoryNode
end

treeviewCategory.AfterSelect = function (sender, e)
    local entries = treeviewCategory.SelectedNode.Tag
    if entries ~= nil then
        listviewEntries.Items.Clear()
        for _,entry in ipairs(entries) do
            AddEntryToListView(entry)
        end
    end
end

function AddEntryToListView(entry)
    local item = Gui.ListViewItem{ Text=entry[1] }
    item.SubItems.Add(entry[4])
    item.SubItems.Add(entry[5])
    item.Tag = entry
    listviewEntries.Items.Add(item)
end

function DeleteEntry()
    local item = listviewEntries.SelectedItems[1]
    local entry = item.Tag
    if entry ~= nil then
        local categoryNode = FindCategoryNode(entry)
        table.removeitem(categoryNode.Tag, entry)
        table.removeitem(Entries, entry)
        listviewEntries.Items.Remove(item)
    end
end

btnSave.Click = function (sender, e)
    local sfd = Gui.SaveFileDialog{ Title="Save data", Filter="data file(*.dat)|*.dat" }
    if sfd.ShowDialog() == "OK" then
        local file = io.open(sfd.FileName, "w")
        file:write("Entries = {\r\n")
        for _,entry in ipairs(Entries) do
            file:write('{"', table.concat(entry, '", "'), '"},\r\n')
        end
        file:write('}')
        file:close()
    end
end

btnOpen.Click = function (sender, e)
    local ofd = Gui.OpenFileDialog{ Title="Open data file", 
			Filter="data file(*.dat)|*.dat" }
    if ofd.ShowDialog() == "OK" then
        dofile(ofd.FileName)
        incomeNode.Nodes.Clear()
        outgoNode.Nodes.Clear()
        dialog_Category.Items.Clear()
        for _,entry in ipairs(Entries) do
            UpdateCategoryTree(entry)
        end
        treeviewCategory.ExpandAll()
        listviewEntries.Items.Clear()
    end
end

Gui.Run(form)

The Ledger Sheet form:

LuaInterpreter/ledger.png

The Add Entry dialog:

LuaInterpreter/addentry.png

Tips to Run Lua Code File

The first method is in Visual Studio project properties, set file name as command line parameter in Debug tab page.

In this way, the lua code can be debugged indirectly through the execution of interpreter.

LuaInterpreter/StartOptions.png

The second method is in Windows file explorer, drag .lua file to lua.exe, it will start lua.exe and pass .lua file as a parameter, the same works for .wlua file and wlua.exe.

LuaInterpreter/DragOpen.png

The third method is when you ship the software, compiling the interpreter will lua code file path hard coded.

Matters Need Attention

The possible usage of this project is use Lua as a data description language or a script language for a .NET application. Currently the code is just enough for a demo, many features are incomplete and untested, you may need to do it by yourself if you want to include the Lua interpreter in a large project.

History

  • 2011-07-19 Initial release
  • 2012-09-22 Fix bug in parse "i == -1" 
  • 2012-09-23 Fix bug in parse "t={f=function() end}