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.

  1. 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 provided keys list. Attempting to set an invalid key should raise an error.

  2. 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