Lab 3 - Lua
This lab will introduce you to the logistics of programming in Lua. To get the starter code for this lab, go to the assignments repository and run git pull
, then go into the lab3
directory.
Installation
Follow the setup instructions in Assignment 8.
To run a Lua file, it’s as simple as lua your_file.lua
.
Basics
Most of Lua should look pretty familiar to you as a standard imperative, dynamically-typed language.
local primitive_types = {
0, -- number
false, -- boolean
"hello", -- string
nil -- nil
}
Functions are first-class values.
local f = function(n) return n + 1 end
assert(f(1) == 2)
local do_twice = function(f)
f()
f()
end
do_twice(function() print('hello!') end)
Control flow is the usual ifs and loops.
for i = 1, 10 do
if i % 2 == 0 then
print(i)
end
end
Most of the language’s complexity and power arises around tables.
local t = {}
t["key"] = "value"
assert(t["key"] == "value")
Lua’s iterators are a common way to deal with tables.
local t = {}
t[1] = "a"
t.b = "c"
-- pairs gets all key/value pairs in the table
-- prints (1, a) and (b, c)
for k, v in pairs(t) do
print(k, v)
end
-- ipairs gets the integer key/value in the table
-- prints (1, a)
for k, v in ipairs(t) do
print(k, v)
end
Lastly, a quick recap on a few pieces of syntactic sugar that Lua provides.
local t = {}
-- These are all the same
t.f = function(self, x) end
function t.f(self, x) end
function t:f(x) end
-- These are both the same
t.f(t, 1)
t:f(1)
Exercises, part 1
To warm up with Lua’s datatypes, let’s dive into an example of Lua’s reflective powers by implementing an auto-serializer. One of Python’s most useful features is its pickle module that enables you to serialize almost any Python object into a string and back. Let’s write a simple equivalent for Lua! The goal is that, for any value v
, we have that deserialize(serialize(v)) == v
where serialize
returns a string.
In part1/serialize.lua
, your task is to implement the serialize
and deserialize
functions. Your serializer only needs to work for numbers, strings, and tables containing keys/values that are numbers/strings/tables. Moreover, you can assume the strings will not contain parentheses. A few hints:
- To perform the necessary introspection, you will need the
type
function. - We have provided you a function
parse_parens
that takes a string with a sequence of balanced parentheses, and returns a list of sub-strings. For example,"(a)((b)(c))(d)"
parses to{"a", "(b)(c)", "d"}
. This should suggest a serialization strategy! tonumber
converts a string to a number.string.format
does string formatting.
Metatables
As we saw in lecture on Wednesday, metatables enable us to override behaviors of tables. We looked specifically at the __index
metamethod that is called whenever a table has a missing index.
local t = {a = 1}
setmetatable(t, {__index = function(t, k)
return 0
end})
assert(t.a == 1)
assert(t.b == 0)
Today, I want to briefly highlight a second metamethod that you will use in the assignment: __newindex
. This metamethod is called when assigning a value to a key that doesn’t exist yet. For example:
local t = {a = 1}
setmetatable(t, {__newindex = function(t, k, v)
rawset(t, k, v + 1)
end})
t.a = 2
t.b = 2
assert(t.a == 2)
assert(t.b == 3)
This function adds one to all values being assigned to the table for the first time. Because t.a
is already defined, newindex is not called for t.a = 2
, but it is called for t.b = 2
. Note that we have to use rawset
, which means “set this key but bypass any __newindex
” because otherwise doing t.k = v + 1
would cause an infinite recursion.
Exercises, part 2
In part2/metatables.lua
, we have two short exercises for you.
-
Implement a function
guard(t, keys)
that takes as input a table and a list of keys, and augments the table such that newly inserted values must have a key in the providedkeys
list. Attempting to set an invalid key should raise an error. -
Implement a function
multilink(t, parents)
that dispatches lookups for missing keys to a list of parent tables in left-to-right order.
For both functions, see the tests below the function for usage examples.
C API
Read through the Lua C API and C->Rust sections of the assignment.
Exercises, part 3
For the final exercise, you will implement a short string matching function in Rust that you can call in Lua. In part3/stringmatch.rs
, we have provided you with a skeleton for a Rust-style Lua library containing a single function, stringmatch
.
stringmatch
should be a Lua function that takes two arguments, a string and a pattern, and returns the number of times that pattern appears in the string. For example:
assert(stringmatch.match("abc", "a") == 1)
assert(stringmatch.match("aaa", "a") == 3)
assert(stringmatch.match("abc", "d") == 0)
assert(stringmatch.match("hello world", "world") == 1)
From the Lua API, you will need to use luaL_checkstring
and lua_pushnumber
to implement this function. You should also investigate the CStr
API for dealing with C string pointers in Rust. Run make
to build the Rust file into a Lua library.
In order to build you will need to rename a file depending on whether you are on Linux or macOS.
# Linux
mv lauxlib.linux.rs lauxlib.rs
# macOS
mv lauxlib.macos.rs lauxlib.rs