Types, truthiness, and values
Luau lets a project add static information gradually. Pop starts from a different contract: every reachable value and operation must have a resolved static meaning before execution.
This is not merely “Luau with more annotations.” It changes which programs can be expressed and where uncertainty must appear.
Inference is not dynamic typing
Section titled “Inference is not dynamic typing”Both languages can infer a local:
local score = 10In Pop, that inference permanently chooses a type for the local. The declaration behaves as though the type were written:
local score: Int = 10Later assignments may change the stored Int, but not the type:
score = 25
-- Rejected:-- score = "twenty-five"There is no any escape that postpones this operation until runtime. If several values genuinely represent different cases, model those cases with an enum or tagged union instead of erasing their types.
Type names are Pop types
Section titled “Type names are Pop types”Some common ideas use different names:
| Luau type | Usual Pop direction |
|---|---|
boolean |
Boolean |
string |
String |
number |
choose an integer or floating-point primitive |
nil |
nil, normally as part of T? |
{T} array-shaped table |
{T} or Array<T>, with fixed length |
{[K]: V} |
{[K]: V} or Table<K, V>, with stricter keys and operations |
| structural table object | record, class, or interface |
any / unknown |
no direct Pop equivalent |
Similar punctuation does not imply identical runtime behavior. In particular, Pop’s {String} is a dedicated array type rather than shorthand for a general table convention.
Conditions answer one Boolean question
Section titled “Conditions answer one Boolean question”Luau conditionals accept values of every type and interpret only false and nil as false. Pop conditions accept Boolean only.
Instead of:
if count then print(count)endstate the intended question:
if count > 0 then print(count)endThis distinction matters because the Luau form hides several possible meanings:
- “is the count positive?”
- “is the count nonzero?”
- “was a count supplied?”
- “did an operation succeed?”
Those questions may require different types and comparisons. Pop makes the program choose.
Logical operators stay Boolean
Section titled “Logical operators stay Boolean”In Luau, and and or are often used to select and propagate arbitrary operand values. In Pop, they are Boolean operators:
local shouldStart = enabled and readylocal shouldStop = failed or cancelledUse an if expression to choose a non-Boolean value:
local label = if ready then "ready" else "waiting"Both branches must have one exact static type.
Absence has a visible type
Section titled “Absence has a visible type”A Pop optional type states that a value may be absent:
function lookup( scores: {[String]: Int}, name: String,): Int? return scores[name]endThe result Int? is shorthand for Int | nil. A caller cannot pass it directly where an Int is required.
This differs from letting nil appear through any value path. The function signature identifies the uncertainty at its source.
rc.3 optional boundaries
Section titled “rc.3 optional boundaries”The foundation is intentionally limited in this release:
- full flow narrowing is not implemented;
- there is no general
?propagation or unwrapping operator; - a standalone
nilliteral is not implicitly widened into every annotated optional; - optional-valued table assignment is not deletion.
Prefer an API whose result honestly stays optional, or use a checked operation after establishing its precondition. Do not invent a fake default merely to make the question mark disappear.
Choose the numeric meaning
Section titled “Choose the numeric meaning”Luau’s common number model does not tell the reader whether a value is an index, byte count, signed balance, or fractional measurement. Pop provides fixed numeric primitives.
local retryCount: UInt8 = 3local balance: Int64 = -250local completion: Float64 = 0.75Signed integers:
Int8 Int16 Int32 Int64Unsigned integers:
UInt8 UInt16 UInt32 UInt64Floating point:
Float32 Float64Int and Byte are convenient aliases documented in the primitive section.
Conversions are explicit
Section titled “Conversions are explicit”Different widths do not silently merge:
local small: Int16 = 120local wide: Int64 = Int64(small)local decimal: Float64 = Float64(wide)Target-type calls make the conversion visible and checked. A value outside the target’s range traps rather than wrapping or relying on an implicit coercion.
When porting a number, ask:
- Can it be negative?
- Can it contain a fraction?
- What range is valid for the domain?
- Does it cross a public function or data boundary?
The answers select a type more reliably than copying the original annotation.
String operations are explicit
Section titled “String operations are explicit”Keep .. for two strings:
local fullName = first .. " " .. lastUse backtick interpolation when values appear inside a larger message:
local message = `{name} has {score} points`Use String(value) for a supported primitive conversion:
local message = "score=" .. String(score)Do not use + for strings. It remains a numeric operator, and Pop does not call a metamethod or universal tostring fallback.
Equality is type-directed
Section titled “Equality is type-directed”== and ~= remain recognizable, but equality support belongs to the operands’ resolved types.
Primitive values use their defined value equality. Records compare structurally when every field supports equality. Classes compare managed identity. Arrays, tables, and functions do not receive an invented structural equality in rc.3.
That means a Luau comparison should be reviewed for intent:
- compare record data when value equality is meaningful;
- compare a class when object identity is meaningful;
- write a deliberate element comparison when a collection’s contents matter.
Constants are not mutable globals
Section titled “Constants are not mutable globals”Pop constants are declarations whose values are evaluated under the compile-time system. A usable runtime constant still has a fixed declared identity and type.
They do not create a mutable global environment where any source file can add or replace names. Runtime state belongs in locals, managed objects, and values passed through typed functions.
A conversion exercise
Section titled “A conversion exercise”Start with this Luau fragment:
local value = getValue()
if value then print("value=" .. value)endDo not translate it yet. First answer:
- What exact values can
getValuereturn? - Is absence possible, or is the condition testing a numeric/string property?
- What type does
printneed? - Is conversion to text part of the function’s contract or only presentation?
Possible Pop designs include:
getValue(): Intplusif value > 0 then;hasValue(): Booleanand a separate checked value operation;getValue(): Int?preserved as optional through the calling API;getLabel(): Stringif the operation’s real purpose is presentation.
The correct translation depends on meaning that Luau allowed the source to leave implicit.
What to keep and what to replace
Section titled “What to keep and what to replace”Keep:
- local inference for obvious initializers;
Boolean, numeric, and text operations once their types are known;==,~=,and,or, andnotwhere operands are supported;..and backtick interpolation for text composition.
Replace:
- changing a local from one type to another;
anyas an integration shortcut;- truthiness as a substitute for a domain question;
- one universal
numberassumption; - implicit string coercion;
- untracked
nilpaths.
Next, Functions and control flow shows which Luau-shaped blocks transfer and where Pop requires exact arity, results, and iteration behavior.
