Modules & require
A LuaScript module is a file that returns a value. require finds the file, compiles it, runs it once, and caches what it returned. This is the Lua 5.4 module system, with .lsc as the preferred extension, a search path that starts at your script's directory, and a small git-based package manager on top.
Writing a module
A module is an ordinary chunk. It usually builds a table of functions and returns it:
local M = {}
function M.square(x)
return x * x
end
function M.clamp(x, lo, hi)
if x < lo then return lo end
if x > hi then return hi end
return x
end
M.PHI = 1.6180339887
print("[mathx] module body executed")
return MLocals in the module stay private. Only what you put on the returned table is visible to callers.
Using require
local mathx = require("mathx")
print(mathx.square(7))
print(mathx.clamp(42, 0, 9))
local again = require("mathx") -- cached: the body does not run again
print(again == mathx)
print(package.loaded.mathx == mathx)[mathx] module body executed
49
9
true
truerequire(name) does the following:
- If
package.loaded[name]is set, return it. Nothing runs. - If
package.preload[name]holds a loader, call it. This is how native modules arrive. - Otherwise search
package.pathfor a file, compile it, and run it with two arguments: the module name and the resolved file path (available as...). - Store the chunk's return value in
package.loaded[name]and return it. A module that returns nothing is stored astrue.
Because of step 1, every require of the same name returns the same value, and the module body runs exactly once per VM. A module that requires itself, directly or through a cycle, gets the error loop or previous error loading module.
package.path and searchpath
package.path is a ;-separated list of templates. require replaces ? in each template with the module name, with dots turned into /, and uses the first file that exists.
package.searchpath(name, path) runs the same search without loading anything, which is handy for diagnostics. It returns the file path, or nil and a list of every place it looked:
print(package.searchpath("geometry.vec", "./?.lsc;./?/init.lsc"))
print(package.searchpath("nope", "./?.lsc;./?/init.lsc"))./geometry/vec.lsc
nil
no file './nope.lsc'
no file './nope/init.lsc'require reports the same list when it fails. You can read and modify package.path at run time; changes affect later require calls.
dofile(path) runs a file every time it is called, with no caching. loadfile(path) compiles a file and returns the chunk as a function without running it, and load(source) does the same for a string.
Module resolution
The default package.path is searched in this order:
| Order | Templates | Where it comes from |
|---|---|---|
| 1 | <script dir>/?.lsc, ?.lua, ?/init.lsc, ?/init.lua | Added automatically for the script being run |
| 2 | ./?.lsc, ./?.lua, ./?/init.lsc, ./?/init.lua | Working directory |
| 3 | ./src/?.lsc (and the same four forms) | Working directory |
| 4 | ./lua_modules/?.lsc (and the same four forms) | Packages installed by luascript pkg |
| 5 | $LUASCRIPT_LIB/?.lsc (and the same four forms) | Only when LUASCRIPT_LIB is set |
The script-directory entry means a module next to your script is always found, no matter which directory you launch from:
luascript examples/07_modules.lsc # from the repo root
cd examples && luascript 07_modules.lsc # from the examples directoryBoth find examples/mathx.lsc. .lsc is tried before .lua at every location, so plain Lua modules can be required too.
Packages: dotted names and init files
A dotted module name maps to a subdirectory, and a directory with an init.lsc file can be required by its own name. Given this layout:
geometry/
init.lsc
vec.lsc
pkgs.lsclocal name, path = ...
local vec = {}
function vec.len(x, y) return math.sqrt(x * x + y * y) end
vec.loadedAs = name
return veclocal geometry = {}
geometry.vec = require("geometry.vec")
return geometrylocal vec = require("geometry.vec") -- geometry/vec.lsc
print(vec.len(3, 4), vec.loadedAs) --> 5.0 geometry.vec
local geometry = require("geometry") -- geometry/init.lsc
print(geometry.vec == vec) --> trueThe init file re-exports its submodules, so callers can write require("geometry").vec without a second require. Both names resolve to the same cached table.
Library roots with LUASCRIPT_LIB
LUASCRIPT_LIB points require at a shared library directory. It is read once at VM startup, and appends four templates to the end of package.path:
$LUASCRIPT_LIB/?.lsc
$LUASCRIPT_LIB/?.lua
$LUASCRIPT_LIB/?/init.lsc
$LUASCRIPT_LIB/?/init.luaThe repository's examples/stdlib/ is a small library set laid out this way: strings resolves to strings.lsc, collections to collections/init.lsc, and collections.list to collections/list.lsc.
# bash
LUASCRIPT_LIB=./examples/stdlib luascript examples/08_stdlib.lsc
# PowerShell
$env:LUASCRIPT_LIB="./examples/stdlib"; luascript examples/08_stdlib.lscA relative LUASCRIPT_LIB resolves against your working directory, not the script's.
Native and script modules
There are two kinds of module, and require treats them the same way:
- Script modules are
.lscor.luafiles found onpackage.path. - Native modules are written in Go and ship inside the interpreter:
json,http,db,crypto,time,std,queue, the data-science modules, and more. They are registered inpackage.preloadat startup, so they need no path entry and are found before any file. See Native modules for the list.
local json = require("json")
print(json.encode({ 1, 2, 3 })) --> [1,2,3]A few libraries are also available as globals without a require, as in Lua 5.4: string, table, math, coroutine, os, io and utf8. The math and io globals are smaller than the require-able modules of the same name. See Standard library.
How the type checker sees require
When you call require with a literal string that names a native module, the checker gives the result that module's declared type. Misusing it is a compile error:
local json = require("json")
print(json.encodee({}))luascript: type "{ encode: (any, any) -> string, decode: (string, any) -> any, null: any, empty_array: any, VERSION: string }" has no field "encodee" at line 2Everything else is any: script modules, and native modules required through a variable (require(name)). There is no cross-module type checking yet, so a typo in a field of a script module surfaces at run time instead:
local mathx = require("mathx")
print(mathx.squar) -- no compile error; prints nilThe compile cache
Compiling a file is cached on disk. luascript file.lsc, require and loadfile look up the compiled bytecode by the SHA-256 of the source text, the interpreter version, the bytecode format, and the interpreter binary itself. A hit skips lexing, parsing, type checking and code generation entirely.
- Entries live under your user cache directory, in
luascript/bytecode/<hash>.lscb. - They are written atomically, and only after a fully successful compile, so a file with type errors is never cached.
- A corrupted or stale entry is ignored silently and the file is recompiled.
- The REPL,
load()on strings,-disand bundled executables do not use the cache.
| Variable | Effect |
|---|---|
LUASCRIPT_NOCACHE=1 | Disable the cache |
LUASCRIPT_CACHE_DIR=<dir> | Store cache entries in <dir> instead |
Because the key is the file's content, you never need to clear the cache after editing a file.
The package manager
luascript pkg fetches third-party script packages from git into ./lua_modules/, which is already on package.path. It records them in two files in the current directory.
| Command | Effect |
|---|---|
luascript pkg add <host/path[@ref]> [name] | Clone a package, record it in luascript.json, and pin the commit in luascript-lock.json |
luascript pkg install (or i) | Fetch every dependency in luascript.json that is not already in lua_modules/, at its locked commit |
luascript pkg remove <name> (or rm) | Delete lua_modules/<name> and drop it from both files |
luascript pkg add github.com/user/lsc-strings@v1.2.0
luascript pkg add github.com/user/lsc-strings strings # install under a different name
luascript pkg installA spec is a host and path, not a URL: github.com/user/repo, optionally followed by @ and a branch, tag or commit. The package is cloned over HTTPS (https://github.com/user/repo.git) with git, which must be on your PATH; the .git directory is removed after the clone. The default name is the last path segment. You then require a package by that name: require("lsc-strings") resolves to lua_modules/lsc-strings/init.lsc, and a dotted name such as require("lsc-strings.util") reaches lua_modules/lsc-strings/util.lsc.
{
"name": "",
"version": "",
"dependencies": {
"lsc-strings": "github.com/user/lsc-strings@v1.2.0"
}
}The lockfile stores, per package, its source, ref, clone URL and exact commit, so pkg install on another machine reproduces the same tree. There is no registry and no version resolution: a dependency is a git repository at a ref.
lua_modules/ is resolved relative to the working directory, so run your program from the directory that holds luascript.json.
See also
- examples/07_modules.lsc and mathx.lsc
- examples/08_stdlib.lsc and examples/stdlib/
- CLI for
luascript build, which bundles a script into a standalone executable