Compiled

See the plain Luau your ShotScript turns into: a Rojo layout on disk, and strict Luau for every class, module and func.

You never edit compiled Luau, but knowing its shape helps when you read an error message or the scripts in Studio. Each section below shows one kind of file and the Luau the compiler writes for it.

Where files go

Everything lands in ReplicatedStorage.Shot, at the same path as on disk. On disk (shot build) it uses Rojo names. A system folder, Wallet/, becomes:

init.luauthe folder's entry point: a wrapper ModuleScript called Wallet
Wallet.luauWallet.shot, a child of the wrapper
Bank.luauevery other file, also a child of the wrapper
Types.luauthe Types module
BuyRequest.model.jsonBuyRequest.instance: {"className": "RemoteEvent"}

Scripts, such as the two in Init/, are named for their RunContext:

Client.client.luaua @client script: a Script with RunContext Client
Server.server.luaua @server script: RunContext Server
Name.meta.jsonnext to a disabled script: {"properties": {"Enabled": false}}

A folder without an entry point stays a Folder. Because every file of a folder is a child of the same instance, script.Parent means the same thing in all of them.

The wrapper re-exports the system's public types and returns the entry point:

--!strict
local Types = require(script.Types)
export type Reason = Types.Reason
export type Wallet = Types.Wallet
export type Bank = Types.Bank


return require(script.Wallet)

Every module file

Each module file starts with its folder's types and ends by returning what it declares:

--!strict
local Types = require(script.Parent.Types)   the types line
export type Reason = Types.Reason            one line per type of the folder:
export type Wallet = Types.Wallet            export type for public ones,
type WalletClass = Types.WalletClass         type for internal and imported ones

(the plain Luau above the declaration, unchanged except @shot paths)
(the declaration, below)
(the plain Luau after it)

return Bank

--!strict matters: without it, other files see the module's functions as returning any. @shot.X.Y becomes a relative path such as script.Parent.Parent.X.Y.

A class

A class becomes a table with a metatable, its fields listed as comments:

local Shape = {}
Shape.__index = Shape

--@public Name string                  fields, as comments
--@internal _hp number

--[[$constructor]] function Shape.new(name : string) : Shape
    local self : Shape = setmetatable({},Shape) :: any     init() expanded
    ...
end

--[[$public]] function Shape.Damage(self : Shape, amount : number)
    ...
end

--[[$public]] function Shape.Made() : number     a static function: no self
  • The constructor returns the class, so a missing return self is an error there.
  • When Circle extends Shape: local Circle = {}, Circle.__index = Circle, setmetatable(Circle :: any, Shape).
  • A parent in the same folder gets local Shape = require(script.Parent.Shape) added above the class.

Class properties go on the class table:

local Icon = {
    Speed = (10) :: (number),
}
Icon.__index = Icon

(with a parent: local Fancy = setmetatable({ Glow = (2) :: (number), }, Icon :: any))

A module

A module is a typed table, and module is a second name for it:

local Bank : Bank = {} :: any
local module = Bank
Bank.Wallets = {}
Bank.Count = 0

function Bank.Open(player : Player) : boolean
    ...
end

A func, a split module, a diffsource func

A func is a local function. A split module and a diffsource func load the half for the side they run on:

local function NextId(prefix : string) : string ... end

local Market : Market = {} :: any
if RunService:IsServer() then
    Market.Server = require(script.Parent.MarketServer)
else
    Market.Client = require(script.Parent.MarketClient)
end

local UniqueIdentifier : UniqueIdentifier = if RunService:IsServer()
    then require(script.Parent.UIDServer)
    else require(script.Parent.UIDClient)

Split modules and diffsource funcs get local RunService = game:GetService("RunService") above them.

The Types module

Each folder's Types.shot becomes a module that only holds types:

--!strict
if true then return {} end

local Signal = require(script.Parent.Parent.Signal)
--[[$internal]] export type Signal = Signal.Signal

--[[$public]] export type Reason = "Pickup" | "Shop"

--[[$public]] export type Shape = {
    Name : string,
    _hp : number,

    Damage : (self : Shape, amount : number) -> (),
}

--[[$internal]] export type ShapeClass = {
    new : (name : string) -> Shape,
    Made : () -> number,
    Damage : (self : Shape, amount : number) -> (),
}
  • if true then return {} end: the module is only types and never runs.
  • Imports come first, then your own types (Reason).
  • Each class gets two types: its object type (Shape: fields, then methods) and the class table's type (ShapeClass: constructors, statics, methods).

A subclass's class type lists only its own members, so reach an inherited static through the class that declares it (Shape.Made(), not Circle.Made()). A documented member's type is typeof(__docfuncN), a stub function carrying the doc, so editors show it on hover. Every type is export type in the Types module; public and internal only decide what the other files and the wrapper re-export.