LuaScript
Documentation menu

CLI & tooling

Everything LuaScript ships is in one binary, luascript. Run it with a file to execute a script, with nothing to open the REPL, or with a subcommand for the rest of the toolchain: docs, formatter, bundler, analyzer, test runner, profiler, package manager and language server.

usage: luascript [flags] [script.lsc]
 
With no script, starts the REPL.

Subcommands are matched before flag parsing, so the subcommand name must be the first argument: luascript fmt -w app.lsc, not luascript -w fmt app.lsc. Each subcommand has its own flags, and -h on any of them prints them.

Running scripts

luascript app.lsc            # compile and run
luascript                    # no script: start the REPL

If compilation fails (a syntax error, a type error or a <const> violation), nothing runs. Compile errors and uncaught runtime errors are printed with a luascript: prefix, and the process exits with status 1.

Flags

FlagEffect
-iStart the REPL. Takes no script argument.
-vPrint the version and exit.
-disDisassemble the script to a bytecode listing instead of running it.
-timeRun the script, then print Execution time: ....
-watchRun the script, then re-run it every time the file changes. Ctrl+C stops.
-gc-percent NSet Go's GOGC for the run. 0 keeps the default and a negative value disables the GC.
-mem-limit NSet a soft heap memory limit in bytes. 0 means unlimited.
-bonsaiGrow an ASCII bonsai tree. See Bonsai mode.

-time and -watch are mutually exclusive, and both need a script file.

luascript -v
Version: 3.0.0, GitCommit: eb7f827, BuildTime: 2026-10-11T20:53:58Z

Release binaries and make build stamp the commit and build time at link time. A plain go build inside a git checkout falls back to the commit and commit time Go embeds (with a -dirty suffix for uncommitted changes); when neither is available, for example under go run, both read unknown.

-watch polls the file every 200 ms and debounces saves. Each run gets a fresh VM. A compile or runtime error is printed and the watcher keeps going, so fix the file and save again; Ctrl+C stops it.

Disassembling with -dis

-dis compiles the file through the whole pipeline (including type checking and constant folding) and prints the resulting instructions with their source lines. It's the quickest way to see what the compiler did with your code.

add.lsc
local function add(a: number, b: number): number
    return a + b
end
print(add(2, 3))
luascript -dis add.lsc
ProgramStart ProgramStart (0+ params, 0 locals, 0 upvalues, 10 instructions)
     0  [src    1]  closure          0
     1  [src    1]  setlocal         0
     2  [src    4]  getglobal        "print"
     3  [src    4]  markargs
     4  [src    4]  getlocal         0
     5  [src    4]  loadint          2
     6  [src    4]  loadint          3
     7  [src    4]  call             2 -1
     8  [src    4]  call             -1 0
     9  [src    4]  leave
 
  Function add (2 params, 2 locals, 0 upvalues, 5 instructions)
       0  [src    2]  getlocal         0
       1  [src    2]  getlocal         1
       2  [src    2]  add
       3  [src    2]  return           1
       4  [src    1]  leave
 
Function add (2 params, 2 locals, 0 upvalues, 5 instructions)
     0  [src    2]  getlocal         0
     1  [src    2]  getlocal         1
     2  [src    2]  add
     3  [src    2]  return           1
     4  [src    1]  leave

Nested functions are listed inside their parent chunk and then again on their own. The type annotations are gone: types are erased before bytecode generation, so the typed and untyped versions of add compile to the same instructions.

The compile cache

Running a file goes through an on-disk bytecode cache, so an unchanged script skips recompilation on the next run. Entries live under your user cache directory (luascript/bytecode/), keyed by source text, interpreter build and bytecode format. Only successful compiles are cached. The REPL, -dis, load() strings and bundled executables don't use the cache.

Environment variableEffect
LUASCRIPT_NOCACHE=1Disable the compile cache.
LUASCRIPT_CACHE_DIR=pathStore cache entries in path instead.
LUASCRIPT_LIB=pathAdd a library search root for require. See Modules.

The REPL

luascript with no script (or luascript -i) starts an interactive session with history, keyword completion and continuation prompts.

CommandEffect
helpShow commands, key bindings and tips.
doc <topic>Standard-library reference. Same data as luascript doc. A bare doc lists the topics.
resetRebuild the VM, clearing globals and user state.
clearClear the screen.
exit, quitLeave.
KeyEffect
Ctrl+CCancel the current input.
Ctrl+DExit.
Ctrl+RSearch history.
TabComplete keywords and REPL commands.

Bare expressions print their value. Incomplete input, such as an unclosed function, opens a continuation prompt. Type errors are reported with a distinct type-error: prefix. History persists in ~/.lsc_repl_history.

A top-level local survives across inputs: the REPL promotes it to a global at compile time so later inputs can read it. Inside any nested scope (a do block, if, loop or function body), local keeps standard Lua semantics. Start an input with --!nocheck to skip type checking for that chunk.

Subcommands

SubcommandPurpose
doc (alias man)Standard-library reference pages.
fmtFormat source files.
buildBundle a script and the interpreter into one executable.
analyzeStatic analysis: complexity, lint and security findings.
testRun *_test.lsc files.
profileCollect CPU and heap profiles.
pkgManage dependencies.
lspRun the language server on stdio.

doc

Man-page style documentation for every global, library function, module member and object method, rendered from the curated registry in internal/docs/.

luascript doc                 # index of every topic
luascript doc string          # one topic page
luascript doc math.floor      # one entry
luascript doc -k interpolation  # search names, signatures and summaries
MATH(3)                             library                              MATH(3)
 
NAME
       math.floor — The largest integer not greater than x.
 
SYNOPSIS
       math.floor(x): number
 
DESCRIPTION
       The largest integer not greater than x.
 
SEE ALSO
       math, stats, bit32, ndarray, linalg
FlagEffect
-k textSearch topic names, signatures and summaries (apropos).
-allPrint every page.
-listPrint topic names only, one per line.
-auditCompare the docs against a live VM and report undocumented or stale names.
-width NLine width (default 80, or $COLUMNS).
-no-colorDisable ANSI styling.

Flags go before the topic: luascript doc -no-color math.floor.

fmt

A formatter that preserves comments and blank lines.

luascript fmt app.lsc          # print the formatted source to stdout
luascript fmt -w app.lsc       # rewrite the file in place
luascript fmt - < app.lsc      # read from stdin
FlagDefaultEffect
-woffWrite the result back to the file. No effect on stdin.
-width N80Target line width.
-indent N2Spaces per indent level.

You can pass several files at once. The language server uses the same formatter for Format Document.

build

build bundles a script and the interpreter into one standalone executable:

luascript build -o hello.exe hello.lsc
./hello.exe
Hello, LuaScript!

The script is fully compiled at bundle time, so syntax and type errors are caught before the executable is written. The output is a copy of the running luascript binary with the script source, the interpreter version and a magic trailer (LUASCRIPT01) appended. On startup, the binary checks its own tail before parsing any flags. When it finds a trailer, it runs the embedded script with the same VM and native modules.

Current limits:

  • The output targets the host platform. There is no cross-compilation flag.
  • Bundled scripts don't see command-line arguments.
  • The executable is as large as the interpreter itself (tens of megabytes).
  • Antivirus heuristics occasionally flag self-appending executables, the same trade-off PyInstaller and Bun's --compile carry. Code-signing resolves it.

analyze

analyze parses a file and runs a set of read-only passes over the AST: complexity, lint and security. It doesn't execute anything.

lint.lsc
local crypto = require("crypto")
local password = "hunter2"
 
local function fetch()
    local unused = 1
    return "http://example.com"
end
 
print(crypto.md5(password), fetch())
luascript analyze lint.lsc
analysis report
 
metrics:
  lines             10
  functions         1
  total complexity  2
  max complexity    1
 
findings:
  WARNING  line 2     [security/hardcoded-credential] 'password' assigned a hardcoded string — use config or env vars
  WARNING  line 5     [lint/unused-local] local 'unused' is never used
  WARNING  line 6     [security/plaintext-http] plaintext http:// URL — prefer https://
  WARNING  line 9     [security/weak-crypto] crypto.md5 is a weak hash — prefer sha256 or stronger
 
summary: 4 finding(s) — 0 error, 4 warning, 0 info

The rules are unused-local, unreachable-code and shadowing (lint), weak-crypto, plaintext-http and hardcoded-credential (security), and a per-function cyclomatic-complexity check. -max-complexity N sets the complexity threshold (default 10). analyze exits with status 1 when there are findings, so you can use it as a CI gate.

test

test discovers and runs every *_test.lsc file under the given paths (default .). A path that names a file directly runs whether or not it matches the suffix. Each file runs in its own VM. Test files use the built-in test module:

greet_test.lsc
local t = require("test")
 
local function greet(name: string): string
    return `hello, {name}`
end
 
t.describe("greet", function()
    t.test("formats a name", function()
        t.assert_eq(greet("Ada"), "hello, Ada")
    end)
end)
luascript test -v .
greet_test.lsc
  ok    greet/formats a name  <1µs
  1 passed in 2.60ms
 
 
1 file, 1 passed in 2.60ms
PASS
FlagEffect
-run patternRun only tests whose name matches a Lua pattern or substring.
-vReport every test, not just failures.
-failfastStop at the first failure.
-listList the tests that would run, without running them.
-no-colorDisable ANSI colour.

When walking directories, test skips lua_modules/, node_modules/ and any directory whose name starts with . or _. It exits with status 1 if any test fails. The assertion API is covered in Testing.

profile

profile runs a script under Go's pprof and writes the profiles to disk. Use it to find hot spots, or to collect a profile for a profile-guided build of the interpreter.

luascript profile -cpu cpu.prof -count 50 workload.lsc
go build -pgo=cpu.prof -o luascript ./cmd/luascript

After the run, profile prints a summary and the matching PGO build command on stderr:

luascript profile: 50 run(s) in 149.8664ms
  cpu profile: cpu.prof
  pgo build:   go build -pgo=cpu.prof -o luascript ./cmd/luascript
FlagDefaultEffect
-cpu pathcpu.profWrite a CPU profile. An empty string disables it.
-mem pathnoneWrite a heap profile.
-count N1Run the script N times under profiling.
-mem-statsoffPrint a memory-stats summary after the run.

scripts/build-pgo.sh automates the build, profile and rebuild cycle.

pkg

pkg is a small git-based package manager. It records dependencies in luascript.json, pins exact commits in luascript-lock.json, and installs packages into lua_modules/, which is already on the default require search path.

luascript pkg add github.com/user/repo@v1.2.0        # fetch, record and lock
luascript pkg add github.com/user/repo mylib         # install under another name
luascript pkg install                                # fetch everything in luascript.json
luascript pkg remove mylib                           # uninstall and forget
CommandAliasEffect
add <host/path[@ref]> [name]Fetch a dependency and record it. ref is a tag, branch or commit.
installiFetch everything listed in the manifest, honouring the lockfile.
remove <name>rmUninstall and forget a dependency.

A spec is a host/path, not a URL. Fetching shells out to git, which must be on your PATH. After pkg add github.com/user/repo, scripts load the package with require("repo"). See Modules for how require resolves names.

Language server

luascript lsp runs a Language Server Protocol server over stdio. It provides:

  • diagnostics: syntax errors, type errors (honouring the file's mode directive) and analyze findings, refreshed on open, on every change and on save
  • hover documentation
  • completion
  • document symbols (outline)
  • formatting, with the same engine as luascript fmt

Hover and completion for the standard library come from the same internal/docs/ registry that powers luascript doc, so the editor and the man pages never disagree. Pass -log path to write debug logs to a file.

Any LSP-capable editor can launch luascript lsp. The repository includes a VS Code extension in client/ that does it for you and adds syntax highlighting. See Installation for how to build and install it, and the luascript.server.path setting to point it at your own binary.

Bonsai mode

-bonsai is an ASCII bonsai grower. It's a fun side mode and has nothing to do with the language runtime.

luascript -bonsai                      # full-screen; q or Ctrl+C to leave
luascript -bonsai -bonsai-print        # print one tree to stdout
luascript -bonsai -bonsai-live         # animate the growth
luascript -bonsai -seed 42             # reproducible tree
luascript -bonsai -bonsai-msg "hello"  # attach a message
                                                  &&&&       &&&&&_ & &/&&&&& &  &
                                                   &&&&           \__&/&&&/&&_/_/__/
                                                 &   \&            /\/  \   &
                                                     /            /    |_/
                                                     \_\_        \    |_/
                                                        \___      /\___/
                                                           \_\\\///_/
                                                              \_
                                                              \
                                                            \|/
                                             :___________./~~~\.___________:
                                              \                           /
                                               \_________________________/
                                               (_)                     (_)