Coming from Luau
Pop looks familiar to a Luau reader on purpose. Both languages use local, function, if ... then, while ... do, repeat ... until, and blocks closed by end. Both use ~= for inequality and .. for string concatenation.
That resemblance is a reading advantage, not a compatibility promise. Pop is not a Luau dialect, does not run inside the Luau VM, and does not inherit Lua’s table-centered runtime model.
The most useful first rule is:
Keep your control-flow intuition, but rebuild your type, data, module, and runtime intuition.
The short comparison
Section titled “The short comparison”| Area | Luau expectation | Pop rc.3 model |
|---|---|---|
| Typing | gradual typing with dynamic escape routes | static types with no any or dynamic fallback |
| Conditions | every value has truthiness | conditions must be Boolean |
| Numbers | primarily one number type |
fixed signed, unsigned, and floating-point primitives |
| Tables | universal mutable data structure | typed key-value aggregate with a restricted role |
| Arrays | table convention that can grow | separate typed, fixed-length, one-based aggregate |
| Objects | tables, metatables, and structural shapes | records, classes, interfaces, and unions |
| Modules | execute a script through require and receive a value |
compile files into namespaces inside a Bubble |
| Missing data | nil, often used to remove table fields |
optional types; no table deletion in rc.3 |
| Execution | VM-hosted script environment | ahead-of-time native compilation |
| Errors | runtime errors may be caught with Luau facilities | compile-time diagnostics and non-catchable runtime traps |
Use this guide as a mini-book
Section titled “Use this guide as a mini-book”This page gives the complete map. The focused leaves develop each change with smaller comparisons, porting rules, and exercises:
- Types, truthiness, and values replaces gradual and coercive assumptions with Pop’s exact static model.
- Functions and control flow separates familiar block syntax from exact function and loop contracts.
- From universal tables to deliberate data chooses among arrays, tables, records, classes, interfaces, enums, and unions.
- Modules, Bubbles, and native execution replaces
require, exported tables, globals, and host-VM expectations. - Porting a Luau program walks through the design decisions before translating syntax.
Read them in that order when Pop is your first language after Luau. If you only need to correct one expectation, jump directly to the matching leaf.
What you do not need to relearn
Section titled “What you do not need to relearn”Several forms transfer directly enough to reduce the initial syntax burden.
Locals and blocks
Section titled “Locals and blocks”local score = 10
if score >= 10 then print("passed")else print("try again")endlocal, if, then, else, and end have the shape a Luau programmer expects. Scope still follows the surrounding block.
Loop shapes
Section titled “Loop shapes”while running do update()end
repeat retry()until finished
for index = 1, 10 do print(index)endwhile, repeat, and inclusive numeric for ranges are familiar. break and continue operate on the innermost loop.
Familiar logical and text operators
Section titled “Familiar logical and text operators”local allowed = active and not blockedlocal fullName = first .. " " .. last
if left ~= right then print("different")endPop uses and, or, not, ==, ~=, and ... Their operand rules are stricter, but the notation is recognizable.
One-based array positions
Section titled “One-based array positions”Pop arrays begin at position 1, which matches ordinary Luau array conventions:
local names: {String} = { "Ada", "Grace" }local first = Array.get(names, 1)What changes is that a Pop array is its own fixed-length typed aggregate, not just a table being used with consecutive integer keys.
Multiple assignment
Section titled “Multiple assignment”local left, right = 10, 20left, right = right, leftAll right-hand values are evaluated before stores, so swapping remains natural. Pop requires exact arity and compatible static types.
Colon method calls
Section titled “Colon method calls”counter:increment()The call-site shape is familiar. The receiver belongs to a declared class or interface contract rather than being supplied through a metatable convention.
The first mental-model change: types are a closed contract
Section titled “The first mental-model change: types are a closed contract”In Luau, annotations can gradually describe code while some values still pass through any or other dynamic boundaries. Pop has no any, unknown, or dynamic fallback type.
local score = 10score = 20
-- Rejected: score was inferred as Int.-- score = "twenty"Inference saves syntax; it does not make the value dynamic. Once Pop infers score: Int, every later assignment must remain Int.
Function boundaries state their parameter and result types:
function double(value: Int): Int return value * 2endPop checks callers and every reachable return. There is no mode where a failed type relationship becomes an unchecked runtime operation.
Function results are not inferred
Section titled “Function results are not inferred”An omitted result annotation means the function returns no value:
function announce(message: String) print(message)endTo return a value, write its type:
function lengthLabel(length: Int): String return `length={length}`endDo not port a Luau function and assume Pop will infer its public result from return statements.
Parameters are immutable
Section titled “Parameters are immutable”Function parameters cannot be reassigned in rc.3. Create a local working value:
function countDown(start: Int) local current = start
while current > 0 do print(current) current -= 1 endendThe local has one static type but remains mutable.
Conditions are Boolean, not truthy
Section titled “Conditions are Boolean, not truthy”Luau treats only false and nil as false in a condition. Pop does not define general truthiness.
This Luau pattern does not transfer:
if count then print("present")endWrite the actual question in Pop:
if count > 0 then print("positive")endThe condition of if, elseif, while, until, and a conditional expression must be Boolean.
This rule prevents several ambiguous habits:
0does not mean either true or false;- an empty
Stringis not a condition; - an object reference is not a condition;
- an optional value cannot be used as an automatic presence test.
Prefer functions and comparisons that answer a Boolean question explicitly.
nil becomes part of the type
Section titled “nil becomes part of the type”In Luau, nil frequently moves through ordinary code and also removes keys from tables. In Pop, possible absence appears in an optional type:
function findScore(scores: {[String]: Int}, name: String): Int? return scores[name]endInt? means Int | nil. It is not interchangeable with Int.
Pop rc.3 has intentionally limited optional conveniences:
- full optional flow narrowing is not implemented;
- there is no general optional-unwrapping operator;
- a standalone
nilliteral does not currently widen into every optional context; - assigning
nilto a table key is not deletion.
Use checked operations when violating a precondition should trap, and preserve V? in APIs where absence is a real outcome.
Numbers are not one universal number
Section titled “Numbers are not one universal number”Luau programmers commonly think in terms of the number type. Pop exposes exact numeric primitives:
- signed integers:
Int8,Int16,Int32,Int64; - unsigned integers:
UInt8,UInt16,UInt32,UInt64; - floating point:
Float32,Float64; - convenient aliases such as
IntandByte.
local attempts: UInt8 = 3local population: Int64 = 8_000_000local ratio: Float64 = 0.75Mixed numeric widths do not silently become one floating-point value. Use an explicit target-type conversion:
local small: Int16 = 120local wide = Int64(small)local decimal = Float64(wide)Conversions are checked. Integer overflow and invalid conversion follow defined trap behavior instead of inheriting VM coercion rules.
When porting code, decide what the number represents: a count, byte, identifier, measurement, or fractional value. Choose its type from that meaning.
Strings: keep .., replace coercion
Section titled “Strings: keep .., replace coercion”String concatenation remains ..:
local fullName = first .. " " .. last+ is numeric only. It never falls back to string concatenation:
-- Rejected:-- local message = "score: " + scoreUse String(value) for a supported primitive conversion:
local message = "score: " .. String(score)Or use backtick interpolation:
local message = `score: {score}`The interpolation shape should feel familiar to modern Luau users. Pop still checks each embedded expression and does not call an arbitrary metamethod to stringify an object.
Tables stop being the universal answer
Section titled “Tables stop being the universal answer”This is the largest data-model change.
In Luau, one table can serve as:
- an array;
- a dictionary;
- a record-shaped value;
- mutable object state;
- a namespace returned from a module;
- an object with behavior supplied by a metatable.
Pop gives those roles separate types. Choose based on the meaning of the data.
| Need | Pop model |
|---|---|
| Fixed homogeneous positions | Array |
| Runtime associations by typed key | Table |
| Fixed transparent named fields | Record |
| Nominal identity and mutable state | Class |
| Shared nominal behavior | Interface |
| One value from a closed set of cases | Enum or tagged union |
| Compile-time name organization | Namespace |
Do not begin a Pop design with “which table shape should this be?” Begin with “which of these data contracts describes it?”
Pop arrays versus Luau array tables
Section titled “Pop arrays versus Luau array tables”A Pop array has one element type and a fixed length:
local names: {String} = { "Ada", "Grace", "Linus" }local count = Array.length(names)The first position is 1. Ordinary indexing is optional:
local possible: String? = names[1]Use Array.get for a checked, non-optional read when the program has established the index invariant:
if Array.length(names) > 0 then local first: String = Array.get(names, 1) print(first)endIndexed assignment replaces an existing element:
names[1] = "Augusta"It does not append or grow the array. rc.3 does not yet provide a dynamic List<T> API.
Iteration is currently numeric
Section titled “Iteration is currently numeric”There is no generalized for value in values and no ipairs equivalent in rc.3. Visit fixed array positions with an inclusive numeric range:
for index = 1, Array.length(names) do print(Array.get(names, index))endPop tables versus Luau tables
Section titled “Pop tables versus Luau tables”A Pop table chooses exactly one supported key type and one value type:
local scores: {[String]: Int} = { ada = 10, grace = 20,}Insertion and replacement use brackets:
scores["linus"] = 15scores["ada"] = 25Lookup returns the optional value type:
local score: Int? = scores["ada"]The table cannot gain a differently typed field later. It cannot mix an array region, named fields, methods, and arbitrary metadata.
rc.3 table keys are limited to:
Boolean;- fixed signed and unsigned integers;
String.
String keys compare their text contents. Floating-point values and managed aggregate objects are not table keys.
There is no pairs, deletion, or table length API yet
Section titled “There is no pairs, deletion, or table length API yet”General table iteration is not implemented. Neither are key deletion and source-level length/capacity operations.
This Luau idiom is not a Pop deletion operation:
scores["ada"] = nilIn Pop rc.3, table assignment means insert or replace. See Table lookup and mutation for the complete source and runtime behavior.
Use records for fixed data shapes
Section titled “Use records for fixed data shapes”A Luau program may describe a user with a table type and table literal:
type Player = { name: string, score: number,}
local player: Player = { name = "Ada", score = 10,}In Pop, a fixed transparent shape is a record:
private record Player name: String score: Intend
local player: Player = { name = "Ada", score = 10,}Record fields are value-oriented. Produce a changed value with with:
local promoted = player with { score = player.score + 5,}The original player remains unchanged. Record equality compares fields when every field supports equality.
Use classes for identity and mutable state
Section titled “Use classes for identity and mutable state”Luau commonly constructs objects by attaching methods through a metatable. Pop has nominal class declarations instead:
public class Counter private value: Int = 0
public function Counter:increment() self.value += 1 end
public function Counter:current(): Int return self.value endendConstruct and call the class explicitly:
local counter = Counter {}counter:increment()print(counter:current())Classes have managed reference identity. Assignment shares an object rather than copying its fields.
Pop does not expose Lua metatables, __index, metamethods, or prototype chains. Operator behavior cannot be changed by storing special functions under special keys. General class inheritance is also outside the rc.3 supported set; use interfaces for shared nominal behavior.
Interfaces are nominal, not structural table shapes
Section titled “Interfaces are nominal, not structural table shapes”Luau often accepts a value because its inferred table shape contains the required fields. Pop interfaces are explicit nominal contracts.
A class declares the interface it implements. Merely having methods with similar names is not enough. This lets the compiler resolve behavior without dynamic field lookup or accidental structural conformance.
When porting a Luau structural type:
- use a record when it is transparent data;
- use an interface when callers depend on declared behavior;
- use a class when identity and encapsulated mutation matter.
Functions have exact arity and result packs
Section titled “Functions have exact arity and result packs”Calls must supply the declared arguments with matching types. Pop does not silently fill arbitrary missing parameters with nil or ignore extra arguments.
Multiple returns are represented as an exact fixed pack:
function bounds(): (Int, Int) return 1, 10end
local lower, upper = bounds()The receiving assignment must have the right arity. Tuple projection is static and one-based; there is no runtime pack reshaping in rc.3.
Variadic Luau APIs should be redesigned around a fixed signature, array, record, or explicit overload in the current Pop surface.
Generics are explicit
Section titled “Generics are explicit”Pop rc.3 supports generic functions, records, and tagged unions, but calls do not infer type arguments:
private function identity<T>(value: T): T return valueend
local answer = identity<<Int>>(42)The declaration’s <T> should look familiar. The call uses <<Int>> so type arguments remain unambiguous beside comparison operators.
Each reachable concrete use is specialized before backend execution. There is no runtime type table or dynamic generic dispatch.
Modules become namespaces inside Bubbles
Section titled “Modules become namespaces inside Bubbles”A Luau module commonly executes a file and returns a table:
local Math = {}
function Math.double(value) return value * 2end
return MathPop source does not return a namespace table. A file declares a namespace and ordinary typed declarations:
namespace App.Math
function double(value: Int): Int return value * 2endAnother file in the Bubble can use the namespace:
namespace App
using App.Math
function main() print(double(21))endusing affects compile-time name resolution. It does not execute the other file, perform a runtime lookup, or receive a returned table.
A bubble.toml manifest describes the Bubble. Declarations are internal by default, may be private to one module, or may be public to other Bubbles. Pop has no export declaration modifier.
Dependency resolution and a package registry workflow are not implemented in rc.3. A manifest can organize the current project, but it does not reproduce a Luau package loader.
There is no mutable global environment model
Section titled “There is no mutable global environment model”Do not expect names to appear because a host inserted them into a global table. Pop resolves functions, types, constants, namespaces, locals, and parameters statically.
There is also no Pop equivalent of dynamic require by computed string, loadstring, or runtime mutation of a module’s exported table. Program structure must be visible to the compiler.
Runtime and host expectations change
Section titled “Runtime and host expectations change”Standalone Luau and Roblox Luau execute inside hosts that supply a VM and host-specific APIs. Pop rc.3 builds native executables.
Consequently, Pop does not automatically provide:
- Roblox services, Instances, events, tasks, or datatypes;
- a Lua or Luau global library;
- VM reflection and dynamic code loading;
- metatable hooks;
- a host-provided module cache.
Use only the Pop Standard foundation documented for this edition. Reserved names or future library identities are not proof that an API is usable.
Error handling is not pcall
Section titled “Error handling is not pcall”Many mistakes are rejected while checking:
- wrong argument or result types;
- unknown names;
- unsupported operators;
- invalid aggregate fields;
- non-Boolean conditions;
- unsupported table key types.
Valid programs can still encounter checked runtime traps, such as overflow, division by zero, invalid conversion, or a failed checked array access.
rc.3 has no source-level exception, pcall, or xpcall mechanism for catching those traps. Model recoverable absence with optionals and validate preconditions before choosing checked operations.
Translation example: from one Luau table to several Pop types
Section titled “Translation example: from one Luau table to several Pop types”Consider a Luau player object:
local Player = {}Player.__index = Player
function Player.new(name) return setmetatable({ name = name, score = 0, badges = {}, }, Player)end
function Player:award(points) self.score += pointsendA direct syntax translation would miss Pop’s stronger modeling tools. First decide what each part means:
- player identity and mutable score belong to a class;
- fixed constructor state belongs to declared fields;
- a fixed collection of badge positions may use an array;
- runtime badge associations may instead use a typed table;
- methods belong to the nominal class, not a metatable.
One rc.3 model is:
public class Player private name: String private score: Int = 0 private badges: {String}
public function Player:award(points: Int) self.score += points end
public function Player:currentScore(): Int return self.score endend
local badges: {String} = { "Founder", "Reader" }local player = Player { name = "Ada", badges = badges,}
player:award(10)print(player:currentScore())The source is slightly more explicit because the compiler now knows the field layout, mutation types, method receiver, and result types without executing constructor conventions.
A migration checklist
Section titled “A migration checklist”Before translating a Luau component, answer these questions:
- Which values can actually be absent? Give only those values optional types.
- Which numbers are counts or indexes, and which are fractional measurements?
- Is each table acting as an array, dictionary, record, object, module, or sum type?
- Which table roles should become separate Pop declarations?
- Which conditions rely on truthiness instead of a Boolean question?
- Which functions rely on missing arguments, extra arguments, or variadic packs?
- Which methods rely on metatables, structural lookup, or monkey patching?
- Which modules rely on runtime execution order or returned tables?
- Which failures are recoverable absence, and which indicate a broken invariant?
- Which APIs come from Roblox or another Luau host rather than from the language itself?
Answering these before editing syntax prevents the most common false starts.
Common false friends
Section titled “Common false friends”| Familiar form | Do not assume |
|---|---|
local value = ... |
the inferred type can later change |
if value then |
Pop has Luau truthiness |
{ ... } |
every aggregate is one universal table |
values[index] |
the result is always present |
table[key] = nil |
assignment deletes a key |
function f(...) |
missing and extra arguments are freely reshaped |
object:method() |
methods come from metatable lookup |
using Name |
another source file executes like require |
<T> on a declaration |
calls infer the type argument |
| a reserved Standard name | the runtime API already exists |
Recommended reading after this guide
Section titled “Recommended reading after this guide”Read these chapters next, in order:
- Hello, World! for the source and command workflow.
- Variables and functions for exact static boundaries.
- Boolean, nil, and optionals for absence without truthiness.
- Numeric conversions for the fixed numeric model.
- Arrays, tables, records, and classes for deliberate data modeling.
- Namespaces,
using, and visibility for compile-time organization. - Support matrix before searching for familiar Luau library features.
You already know how to read much of Pop’s control-flow syntax. The new work is learning to make each type, absence case, aggregate role, and program boundary explicit enough that the compiler and every reader see the same program.
