Running a project

Run a program once and watch it print, or play a library's scenarios and see how many pass, with shot run.

What shot run does depends on the project's export, the instance named by the export line of its header. A plain Luau file (.luau) is a program; a ShotScript module, class or func is a library. If you changed the project since it was last saved, the run saves it first, so it always plays what you last wrote.

Run a program

Write a few lines of Luau and watch them print. shot run runs the export once, top to bottom, and each print shows the moment it happens.

Greeter is a program in two Luau files. Its header, the first lines of Greeter.shotsystem, names the export:

.shotsystem

shotsystem 1
export Main

Main.luau

local Greeting = require(script.Parent.Words.Greeting)

print("Greeter starts")
for _, name in { "Ada", "Lin" } do
    print(Greeting.For(name))
end
print("Greeter done")

Words/Greeting.luau

local Greeting = {}

function Greeting.For(name: string): string
    if name == "" then
        error("a greeting needs a name")
    end
    return "Hello, " .. name .. "!"
end

return Greeting

Put the files in a folder, the header in a file named .shotsystem, and make the project with shot import GreeterFiles Greeter.shotsystem (see One-file projects). Run it:

shot run Greeter.shotsystem

Greeter starts
Hello, Ada!
Hello, Lin!
Greeter done

shot run --project Greeter.shotsystem is the same command, written like every other command on a project.

Read a failure

When something breaks, the run stops and shows you the way it came. Give Greeter an empty name, { "Ada", "", "Lin" }, and shot run Greeter.shotsystem prints:

Greeter starts
Hello, Ada!
error: Greeter:Greeter.Words.Greeting:5: a greeting needs a name
  at Greeter:Greeter.Words.Greeting:5 function For
  at Greeter:Greeter.Main:5

The error: line gives the project, the instance path from its root (the root is named after the project), the line and the message. Under it come the project's lines the error passed through, innermost first. What printed before the error stays, and shot run exits with 1.

For now a line in a .shot file is counted in its compiled Luau: with Greeting written as a .shot module, the same error on line 10 of Greeting.shot is reported as line 16.

Play a library's scenarios

A library doesn't run on its own: its scenarios run it, and shot run counts the ones that pass. Counter's export is a ShotScript module:

.shotsystem

shotsystem 1
export Counter.Counter

Counter/Counter.shot

types script.Parent.Types

print("Counter loads")

rootdoc {
    Counts things.
}
module Counter = {
    Count : number = 0

    doc { Adds to the count and returns the new count }
    function Add(by : number) : number
        module.Count += by
        return module.Count
    end
}

An empty Counter/Types.shot sits beside it (see Types). The export is the instance path from the root, with dots: export Counter/Counter is refused with Counter.shotsystem exports Counter/Counter, and it has no Counter/Counter.

A scenario is a Luau file in the project's Scenarios folder, Scenarios/<Name>.luau, and never part of the export. Counter has three, and shot run without a name lists them:

shot run Counter.shotsystem

Counter.shotsystem exports the library Counter.Counter: it runs through its scenarios (shot run <file> <name>, <A,B> or all)
  Adds
  Negative
  StartsAtZero

Name the ones to play:

shot run Counter.shotsystem Addsplays Adds
shot run Counter.shotsystem Adds,StartsAtZeroplays those two, in that order
shot run Counter.shotsystem allplays every scenario, in name order

Names match exactly. shot run Adds --project Counter.shotsystem works too.

Scenarios/Adds.luau

local Counter = require(root.Counter.Counter)

Counter.Add(2)
Counter.Add(3)
print("count " .. Counter.Count)
assert(Counter.Count == 5, "two adds should make 5")

shot run Counter.shotsystem Adds prints:

ok     Counter:Scenarios.Adds
  Counter loads
  count 5
1 of 1 scenario(s) passed

Each scenario gets an ok or FAILED line with what it printed under it, and the last line counts the ones that passed.

Catch what's missing

Write down what the code should do, and a failing scenario shows you where it doesn't. Counter lets the count go below 0:

Scenarios/Negative.luau

local Counter = require(root.Counter.Counter)

Counter.Add(-2)
assert(Counter.Count >= 0, "the count went below 0")

shot run Counter.shotsystem all plays all three and exits with 1:

ok     Counter:Scenarios.Adds
  Counter loads
  count 5
FAILED Counter:Scenarios.Negative
  Counter loads
  error: Counter:Scenarios.Negative:4: the count went below 0
    at Counter:Scenarios.Negative:4
ok     Counter:Scenarios.StartsAtZero
  Counter loads
  count 0
2 of 3 scenario(s) passed

A name that isn't there fails the same way, with no scenario Nope in Counter.shotsystem (Adds, Negative, StartsAtZero).

Every run starts clean

No scenario leans on another, and no run on the one before. Each program run and each scenario loads every project again, so each module's body runs once in it and its state starts as the code says.

Scenarios/StartsAtZero.luau

local Counter = require(root.Counter.Counter)

print("count " .. Counter.Count)
assert(Counter.Count == 0, "the count should start at 0")

shot run Counter.shotsystem Adds,StartsAtZero prints:

ok     Counter:Scenarios.Adds
  Counter loads
  count 5
ok     Counter:Scenarios.StartsAtZero
  Counter loads
  count 0
2 of 2 scenario(s) passed

Counter loads once for each scenario, and StartsAtZero sees 0 right after Adds made 5. Inside one run, every require of a module returns the same table.

What a scenario sees

Drive the project from outside, with its whole tree in reach. A scenario's code gets three names: script, the scenario itself; root, the project's root, named after the project, with nothing above it; and require, which takes an instance. Only scenarios get root: a module reaches the rest of its project from script.

Scenarios/Sees.luau

print("script is " .. script.Name .. ", in " .. script.Parent.Name)
print("root is " .. root.Name .. ", parent " .. tostring(root.Parent))
for _, child in root:GetChildren() do
    print("  " .. child.Name .. " (" .. child.ClassName .. ")")
end

Add it with shot write Scenarios/Sees.luau --project Counter.shotsystem (the text on stdin; see The shot command), then shot run Counter.shotsystem Sees prints:

ok     Counter:Scenarios.Sees
  script is Sees, in Scenarios
  root is Counter, parent nil
    Counter (ModuleScript)
1 of 1 scenario(s) passed

The folder Counter is a ModuleScript because it holds a module of its own name (see Compiled). A program can have scenarios too: shot run Greeter.shotsystem Hello plays Scenarios/Hello.luau instead of the program.

Reach other code

Pull in your own modules by path, a service by name and another project by its alias. A run answers three kinds of require:

require(script.Parent.Words.Greeting)    -- a module of this project, by its instance path
require("@shotsystem/ProcessService")    -- a service
require("@Bank")                         -- another project's export, by the alias in this project's header

In a .shot file, shot check doesn't know the two string requires yet and reports Unknown require: unsupported path, though the file runs. A .luau file checks clean.

Call a service

Start programs and read what they print. ProcessService is the one service today (see ProcessService):

Main.luau

local ProcessService = require("@shotsystem/ProcessService")

local result, why = ProcessService.RunAsync("python", { "-c", "print(6 * 7)" })
if not result then
    error("couldn't run python: " .. tostring(why))
end
print("python says " .. result.Stdout)

shot run prints python says 42.

Use another project

Build on a library without copying it in. Bank is a library project whose export is Bank/Bank.shot.

Bank's header:

shotsystem 1
export Bank.Bank

Bank/Bank.shot

types script.Parent.Types

rootdoc {
    Keeps a balance of coins.
}
module Bank = {
    Balance : number = 0

    doc { Puts coins in and returns the new balance }
    function Deposit(amount : number) : number
        module.Balance += amount
        return module.Balance
    end
}

Shop is a program beside it. An external line in its header names Bank's file, relative to Shop's own, and gives it an alias.

Shop's header:

shotsystem 1
export Main
external "Bank.shotsystem" as Bank

Main.luau

local Bank = require("@Bank")

print("balance " .. Bank.Deposit(5))
print("balance " .. Bank.Deposit(7))

shot run Shop.shotsystem prints:

balance 5
balance 12

require("@Bank") returns Bank's export, and only the export crosses over: it must be a library, and no other module of Bank can be required from Shop. Every project in a run that uses Bank shares one copy of it.

What stops a run

These mistakes stop a run at once, with a message that names the problem.

A write to _G, which is read only, so modules can't pass state through it:

error: Notes:Notes.Main:2: attempt to modify a readonly table

A require loop, here Cart requiring Prices and Prices requiring Cart. The message gives the chain:

error: require loop at run time: Shop:Shop.Cart -> Shop:Shop.Prices -> Shop:Shop.Cart

A service that isn't there:

error: Notes:Notes.Main:1: no ShotSystem service NoSuchService

An alias with no external line in the project's own header:

error: Notes:Notes.Main:1: require("@Bank"): Notes.shotsystem has no external of that name (a string require is @<Alias> from the project's header, or @shotsystem/<Service>)

An external whose export is a program:

error: Cart:Cart.Main:1: "Greeter.shotsystem" exports the script Main, and a script can't be used from another project: only a module or a class can

A module of another project that isn't its export, even one reached through an instance the export hands out:

error: Cart:Cart.Main:3: a project can't require another project's modules: Bank:Bank.Bank.Ledger

Exit codes

0the program ended, every scenario played passed, or the scenarios were listed
1the program stopped on an error, a scenario failed or isn't there, or the project couldn't be run