Instances

ShotInstance, the base of every object a program touches, with its tree, signals, attributes, tags, Destroy, Instance.new and Clone.

In System and Generic projects, everything a program touches is a ShotInstance: its tree's folders, values and scripts, the services and their handles (an open file, a timer). They work like Roblox's instances, in Roblox's words.

Each example replaces the Main.shot of shot new Demo.ssp (shot write Main.shot --project Demo.ssp, the text on stdin), and shot run Demo.ssp prints what follows it. shot check --project Demo.ssp passes unless an error is shown.

What every instance has

  • Name, Parent (a ShotInstance or nil) and ClassName (read only).
  • Roblox's tree methods: GetChildren, GetDescendants, FindFirstChild, FindFirstAncestor, IsA, ...
  • Attributes and tags: SetAttribute, GetAttribute, GetAttributes, AddTag, HasTag, ...
  • Events: Changed, ChildAdded, ChildRemoved, AncestryChanged, AttributeChanged, Destroying.
  • Destroy() and Clone().

The class pages list every member.

local list = Instance.new("Folder")
list.Name = "List"
local first = Instance.new("StringValue", list)
first.Name = "First"
first.Value = "one"
print(list.ClassName .. " " .. tostring(list.Parent) .. " " .. first:GetFullName() .. " " .. tostring(first))
print(first)
print(typeof(first) .. " " .. tostring(first:IsA("ValueBase")) .. " " .. tostring(first:IsA("ShotInstance")))
print(tostring(list:FindFirstChild("Second")) .. " " .. #list:GetChildren())
local found = list:FindFirstChild("First") :: StringValue?
print("found: " .. (if found then found.Value else "none"))

prints:

Folder nil List.First First
First
userdata true true
nil 1
found: one

print and tostring give the Name. IsA is true for the class, each class above it, "ShotInstance" and "Instance". Three traps:

  • typeof is "userdata" for every class: use IsA.
  • IsA doesn't narrow the type for shot check: after found:IsA("StringValue"), found.Value is still TypeError: Key 'Value' not found in external type 'ShotInstance'. Cast, as above: :: StringValue?.
  • A property or method hides a child of the same name (Roblox's rule): reach a child called Name with FindFirstChild("Name").

Properties and the value classes

BoolValue, IntValue, NumberValue, StringValue, ObjectValue, Color3Value and Vector3Value have one property, Value. Wrong types, read-only properties and missing members error in Roblox's words; :: any gets past shot check, which catches them first:

local count = Instance.new("IntValue")
count.Value = 3
print(select(2, pcall(function() (count :: any).Value = "three" end)))
print(select(2, pcall(function() (count :: any).ClassName = "Counter" end)))
print(select(2, pcall(function() return (count :: any).Nope end)))
local pointer = Instance.new("ObjectValue")
pointer.Value = count
print(tostring(pointer.Value == count) .. " " .. count.Value)

prints:

Main.shot:3: Unable to assign property Value. number expected, got string
Main.shot:4: Unable to assign property ClassName. Property is read only
Main.shot:5: Nope is not a valid member of IntValue "IntValue"
true 3

A caught error starts with its file and line.

Roblox's datatypes keep Roblox's spelling: Vector2, Vector3, CFrame, Color3 (new, fromRGB, fromHex, fromHSV), UDim, UDim2 (new, fromScale, fromOffset), Rect, NumberRange, BrickColor.

local tint = Instance.new("Color3Value")
tint.Value = Color3.fromRGB(255, 128, 0)
print(tint.Value)

prints:

<Color3(1, 0.5019608, 0)>

TweenInfo, Ray and Region3 stay Roblox's:

local info = TweenInfo.new(1)
print(info)

shot check prints:

Demo.ssp/Main.shot:1:14   TweenInfo.new is Roblox's: a System project has no TweenInfo; keep its numbers in a table

Events and signals

use TimeService

local box = Instance.new("Folder")
box.Name = "Box"
local item = Instance.new("StringValue", box)
item.Name = "Item"
local shelf = Instance.new("Folder")
shelf.Name = "Shelf"
box.ChildRemoved:Connect(function(child : ShotInstance) print("Box lost " .. child.Name) end)
shelf.ChildAdded:Connect(function(child : ShotInstance) print("Shelf got " .. child.Name) end)
item:GetPropertyChangedSignal("Parent"):Connect(function() print("Item's Parent signal") end)
item.AncestryChanged:Connect(function(moved : ShotInstance, parent : ShotInstance?)
    print("AncestryChanged: " .. moved.Name .. " is now in " .. (if parent then parent.Name else "nothing"))
end)
local heard = item.Changed:Connect(function(value : string) print("Item's Changed: " .. value) end)
shelf.Changed:Once(function(property : string) print("Shelf's Changed: " .. property) end)
item.Parent = shelf
item.Value = "apple"
item.Value = "apple"
shelf.Name = "Crate"
shelf.Name = "Bin"
heard:Disconnect()
item.Value = "pear"
print("connected: " .. tostring(heard.Connected))
task.spawn(function()
    TimeService:WaitAsync(0.1)
    item.Value = "plum"
end)
print("WaitAsync gave " .. tostring(item.Changed:WaitAsync(1)) .. ", then " .. tostring(item.Changed:WaitAsync(0.1)))

prints:

Box lost Item
Shelf got Item
Item's Parent signal
AncestryChanged: Item is now in Shelf
Item's Changed: apple
Shelf's Changed: Name
connected: false
WaitAsync gave plum, then nil

One parenting fires, in order:

  • ChildRemoved on the old parent, ChildAdded on the new;
  • the Parent property signal, then Changed("Parent") where Changed names properties;
  • AncestryChanged(moved, parent) on the moved instance and each descendant.

A value class's Changed fires only for Value, with the new value; other classes pass the property's name. A set that changes nothing fires nothing. Callbacks run in connect order, each in its own thread.

A signal has:

  • Connect(callback): returns a connection with Connected and Disconnect().
  • Once(callback): the next fire only.
  • WaitAsync(timeout?): the next fire's values, or nil after the timeout, a Destroy or the run's end. A thread waiting with no timeout keeps the run going.

Services' and handles' signals (a started program's Line, RunService.Stopping) are the same.

Attributes and tags

local row = Instance.new("Folder")
row.AttributeChanged:Connect(function(name : string) print("AttributeChanged " .. name) end)
row:SetAttribute("Index", 1)
row:SetAttribute("Index", 1)
row:SetAttribute("Label", "first")
row:SetAttribute("Label", nil)
local all = row:GetAttributes()
all.Index = 99
print(tostring(row:GetAttribute("Index")) .. " " .. tostring(row:GetAttribute("Label")))
print(select(2, pcall(function() row:SetAttribute("bad name", 1) end)))
print(select(2, pcall(function() row:SetAttribute("List", { 1, 2 }) end)))
row:AddTag("Done")
row:AddTag("Done")
print(tostring(row:HasTag("Done")) .. " " .. #row:GetTags())

prints:

AttributeChanged Index
AttributeChanged Label
AttributeChanged Label
1 nil
Main.shot:10: "bad name" is not a valid attribute name (letters, digits and _, at most 100, not starting RBX)
Main.shot:11: SetAttribute List: a table can't be an attribute
true 1
  • Setting the value it has fires nothing; nil removes it.
  • GetAttributes() is a copy.
  • No tables or functions, as in Roblox.
  • Tags are a set.

Destroy

Destroy() is the one way to end an instance:

  • Destroying fires first, with everything still there.
  • Then its descendants are destroyed, its Parent becomes nil and locked, its connections are cut and every WaitAsync on its signals gets nil.
  • A second Destroy() does nothing; any other use errors.

Parent = nil alone keeps an instance alive, to be put back.

use TimeService

local shelf = Instance.new("Folder")
shelf.Name = "Shelf"
local item = Instance.new("StringValue", shelf)
item.Name = "Item"
shelf.Destroying:Connect(function() print("Shelf is being destroyed; it still has " .. #shelf:GetChildren() .. " child") end)
item.Destroying:Connect(function() print("Item is being destroyed") end)
local listening = item.Changed:Connect(function() end)
task.spawn(function() print("WaitAsync resumed with " .. tostring(item.Changed:WaitAsync())) end)
shelf:Destroy()
shelf:Destroy()
TimeService:WaitAsync(0)
print("connected: " .. tostring(listening.Connected))
print(select(2, pcall(function() return item.Name end)))
print(select(2, pcall(function() item.Parent = Instance.new("Folder") end)))

prints:

Shelf is being destroyed; it still has 1 child
Item is being destroyed
WaitAsync resumed with nil
connected: false
Main.shot:15: StringValue "Item" was destroyed
Main.shot:16: StringValue "Item" was destroyed

Making instances

Instance.new(className, parent?) makes an instance, parented when parent is given. A run makes Folder and the value classes; shot check types the result by the name (Instance.new("StringValue") is a StringValue).

shot check also accepts the 48 classes declared for windows, images and more (Window, Frame, BlurEffect, ..., all on the class pages). A run doesn't make them yet, so each gets a note, not an error:

local window = Instance.new("Window")
print(window)

shot check prints:

Demo.ssp/Main.shot:1:16   note: Instance.new("Window") works on the native runtime; today's shot run stops with: Unable to create an Instance of type "Window"
0 errors (1 files checked, 0.3 s)

and shot run:

error: Main.shot:1: Unable to create an Instance of type "Window"
  at Main.shot:1

shot check refuses any other name with how to get one; a typo (Foldr) or a near name (TextButton) gets the close class first:

local service = Instance.new("FileService")
local runner = Instance.new("RunService")
local typo = Instance.new("Foldr")
print(service, runner, typo)

shot check prints:

Demo.ssp/Main.shot:1:17   FileService can't be made with Instance.new: write use FileService
Demo.ssp/Main.shot:2:16   RunService can't be made with Instance.new: write use RunService
Demo.ssp/Main.shot:3:14   Foldr can't be made with Instance.new: write Instance.new("Folder"); it makes Folder, StringValue, NumberValue, IntValue, BoolValue, ObjectValue, Color3Value, Vector3Value, 18 UI classes (Window, Dialog, Frame, TextLabel, ImageLabel, Button ...), 15 image effects (BlurEffect ...) and 15 more (shot docs Instances lists every one)
3 errors in 1 file (1 files checked, 0.3 s)

A name known only at run time is refused by the run:

for _, className in { "FileService", "FileHandler", "PathHandler", "Timer", "Script", "ModuleScript", "Window", "Foldr" } do
    print(select(2, pcall(function() return Instance.new(className :: any) end)))
end

prints:

Main.shot:2: FileService can't be made with Instance.new: write use FileService
Main.shot:2: FileHandler can't be made with Instance.new: PathHandler:OpenAsync() gives one
Main.shot:2: PathHandler can't be made with Instance.new: FileService:GetPathHandler(path) gives one
Main.shot:2: Timer can't be made with Instance.new: TimeService:CreateTimer(interval), TimeService:CreateAlarm(at), TimeService:After(seconds, fn) or TimeService:Every(seconds, fn) gives one
Main.shot:2: Script can't be made with Instance.new: a script comes from the project's tree: add it in the editor and reach it through script or root
Main.shot:2: ModuleScript can't be made with Instance.new: a script comes from the project's tree: add it in the editor and reach it through script or root
Main.shot:2: Unable to create an Instance of type "Window"
Main.shot:2: Unable to create an Instance of type "Foldr"

Clone

Clone() copies an instance with its properties, attributes, tags and descendants, Parent nil. A copied ObjectValue pointing inside the original points at the copy's match; one pointing outside keeps its target. Services and handles such as an open file or a timer can't be cloned. A template in the tree becomes many rows:

Template/Title.instance

StringValue
Value = "row"

Main.shot

use FileService

local template = script.Parent.Template
local list = Instance.new("Folder", script.Parent)
list.Name = "List"
for k = 1, 3 do
    local row = template:Clone()
    row.Name = "Row" .. k
    row:SetAttribute("Index", k)
    row.Parent = list
end
local second = list:FindFirstChild("Row2")
local title = if second then second:FindFirstChild("Title") :: StringValue? else nil
print(#list:GetChildren() .. " rows; Row2's Title: " .. (if title then title.Value else "none"))
local typed = template:Clone() :: typeof(template)
print("typed copy: " .. typed.Title.Value .. ", Parent " .. tostring(typed.Parent))
print(script.Name .. " " .. script.ClassName .. " " .. tostring(script:IsA("ModuleScript")))
print(select(2, pcall(function() return FileService:Clone() end)))

prints:

3 rows; Row2's Title: row
typed copy: row, Parent nil
Main Script true
Main.shot:18: FileService can't be cloned

Clone() returns a plain ShotInstance, so shot check refuses a child of an uncast copy:

local copy = script.Parent.Template:Clone()
print(copy.Title.Value)

shot check prints:

Demo.ssp/Main.shot:2:7   TypeError: Key 'Title' not found in external type 'ShotInstance'

Cast it (:: typeof(template)) or use FindFirstChild.

The run's tree

A program runs on its own copy of the tree (One-file projects, ROOT AND SCRIPT), which is never saved: after the Clone example, shot tree --project Demo.ssp shows no List:

Demo.ssp
./  (the project)
  Main.shot
  Template/
    Title  (StringValue)

Only files the program writes outlive the run (FileService). A .shot file's ClassName is Script, a plain .luau file's Luau; both are IsA("ModuleScript").

Services and handles

Services and their handles are instances too. Reaching a service, ending a handle with Destroy() and what a call gives back: see Services.