Simple by nature. Powerful by design.
Why Pop? • Why not Luau? • Language tour • Runtime • Status • Discord
Pop Lang is a native, strongly and statically typed programming language inspired by Luau.
It keeps the parts that make Luau pleasant to read and write: end-based blocks, lightweight syntax, local inference, first-class functions, closures, coroutines, and convenient collection literals.
The difference is in the foundation. Pop is designed from the beginning for general-purpose software, predictable native runtimes, portable backends, explicit APIs, and static guarantees without a dynamic fallback.
namespace Game.Players
public record Player
name: String
score: Int = 0
end
public function award(player: Player, points: Int): Player
return player with {
score = player.score + points,
}
endPop is not a Lua compatibility layer and does not treat tables as the hidden implementation of every language feature. Records, classes, interfaces, modules, namespaces, arrays, and typed tables are separate concepts with separate semantics.
Important
Pop Lang is under active development and is not yet ready for production use.
Languages with lightweight syntax are often built around a dynamic runtime. Languages with strong static guarantees often introduce more syntax, more punctuation, and more ceremony.
Pop explores another direction:
- source code that stays visually close to Luau;
- every usable value and operation checked before execution;
- native records, classes, interfaces, closures, and collections;
- explicit integer and floating-point types;
- typed errors instead of exceptions as the normal failure path;
- a runtime designed for standalone applications, libraries, tools, services, games, and embedding;
- one language model that can be implemented by native compilers, interpreters, virtual machines, or transpilers.
The goal is not to remove complexity by hiding it at runtime. The goal is to give common programs a small, readable surface while keeping their behavior explicit enough for compilers, editors, runtimes, and developers to agree.
Luau is an excellent language for its primary environment. It improves Lua with a fast type checker, inference, modern syntax, and a strong developer experience for embedded and game-oriented scripting.
Pop has a different goal.
A standalone Luau runtime can change where Luau executes, but it cannot remove the compatibility constraints of the language itself. General-purpose Luau implementations still need to account for features such as gradual typing, any, dynamic tables, metatables, Lua-style module values, and behavior inherited from Lua compatibility.
Those features are useful for scripting and migration, but they make it harder to guarantee that every operation has one statically known meaning across native compilation, embedded runtimes, tooling, and alternative backends.
This is also a risk seen in the JavaScript ecosystem. Better runtimes and TypeScript greatly improve the experience, but they cannot replace JavaScript's dynamic core. Large applications often accumulate extra layers for types, modules, builds, validation, packaging, and runtime compatibility, while dynamic escape hatches remain part of the system.
Pop starts on the other side of that trade-off:
- inference is preserved, but gradual fallback is rejected;
- table literals remain convenient, but tables are not records, classes, modules, or namespaces;
- metatable use cases become native language features or explicit typed protocols;
- modules and dependencies are statically analyzable;
- classes and closures use native runtime representations rather than hidden tables;
- foreign or unstructured data must cross an explicit typed boundary;
- new runtimes do not need to preserve Lua's dynamic object model.
Pop does not aim to clone Luau and then slowly remove its constraints. Each Luau feature is deliberately adopted, adapted, replaced, rejected, or deferred.
The architecture documentation maintains the complete, versioned Luau feature inventory.
Every expression, local, field, parameter, return value, collection element, and call target has a type before the program is executed.
Inference removes repetition:
local name = "Ana"
local score = 10
local active = trueBut inference never becomes a dynamic fallback. Pop has no source-visible value like any or dynamic on which arbitrary operations are allowed.
External and unstructured data must be decoded into a known shape:
public record User
id: UInt64
displayName: String
end
public function decodeUser(json: JsonValue): Result<User, DecodeError>
-- Validate the input and return a typed value.
endPop follows Luau's visual language:
- blocks end with
end; - functions use
function; - locals use
local; - annotations use
name: Type; - methods keep colon-call ergonomics;
- braces represent data and initializers, not executable declaration blocks;
- semicolons are not part of canonical source;
- namespace imports use
using, not JavaScript-style destructuring.
namespace Studio.Gameplay
using Physics = Studio.Simulation.Physics
public function updatePlayer(player: Player, deltaTime: Float)
Physics.step(player, deltaTime)
endusing only affects name resolution. It does not execute code, load a file, or create a runtime object.
Records are typed data, not tables with a convention:
public record Request
path: String
retryCount: Int = 0
end
local nextRequest = request with {
retryCount = request.retryCount + 1,
}Tagged unions represent states that must be handled explicitly:
public union LoadResult
Ready(value: String)
Missing
Failed(message: String)
end
public function printResult(result: LoadResult)
match result
when LoadResult.Ready(value) then
Io.print(value)
when LoadResult.Missing then
Io.print("missing")
when LoadResult.Failed(message) then
Io.print(message)
end
endThe initial match form is exhaustive. Adding a new union case requires callers to decide how it should be handled.
Pop is data-first, not object-oriented-first. Records, unions, functions, and composition should handle most application code.
Classes are available when a value needs identity, encapsulated mutable state, a lifecycle, or runtime polymorphism:
public class Connection
private closed: Boolean = false
public function Connection:close()
if not self.closed then
self.closed = true
-- Release the owned transport.
end
end
endA class is not a table with a metatable. Its fields, visibility, layout, methods, and implemented interfaces are known by the compiler and runtime.
Pop keeps Luau-like collection syntax while giving each collection a real type:
local names: {String} = { "Ana", "Bruno" }
local scores: {[String]: Int} = {
ana = 10,
bruno = 12,
}{T} is an array type. {[K]: V} is a typed associative table. Heterogeneous collections require an explicit union or shared interface.
Tables do not double as modules, records, classes, namespaces, tuples, or universal objects.
Pop does not use one universal IEEE-754 number type.
The language includes fixed-width signed and unsigned integers, Float32, and Float64. Common aliases include:
| Alias | Type |
|---|---|
Int |
Int64 |
Float |
Float64 |
Byte |
UInt8 |
This makes storage, arithmetic, overflow behavior, foreign interfaces, and cross-platform execution easier to reason about.
Expected failures use typed values similar to Result<T, E>:
public function loadConfiguration(path: String): Result<Configuration, LoadError>
-- ...
endpanic is reserved for broken invariants or unrecoverable internal failures. It is not the ordinary application error API.
Pop is designed as a general-purpose language, so the runtime is part of the language model rather than an afterthought attached to a scripting host.
The runtime is responsible for consistent behavior across implementations, including:
- memory management;
- strings and collections;
- closures and captured state;
- classes and interface dispatch;
- coroutines, suspension, and task integration;
- panic, cleanup, and runtime traps;
- foreign-function boundaries;
- platform and standard-library services.
The production runtime design uses a precise concurrent generational garbage collector.
In practical terms:
- short-lived allocations can be collected efficiently in a moving nursery;
- mature objects remain mostly non-moving;
- the collector knows the exact location of managed references;
- native stack frames and runtime structures use precise roots and stack maps;
- write barriers preserve correctness across generations and concurrent work.
Pop does not expose collector-specific object headers as part of the language. Resource cleanup should use explicit ownership and lifecycle APIs rather than depending on when garbage collection happens.
Language features use runtime representations that match their semantics:
- records are records;
- classes have declared layouts and native method dispatch;
- closures use typed environments;
- arrays and tables are distinct collection types;
- interfaces expose only their declared operations;
- namespaces and modules do not become runtime tables.
This gives runtimes freedom to optimize without preserving a universal dynamic table representation.
Pop's canonical MIR is backend-agnostic. It describes typed control flow and runtime operations without tying the language to LLVM, a specific virtual machine, or one garbage collector.
That allows implementations to consume the same semantics for different execution strategies, including:
- native machine-code compilation;
- a reference interpreter;
- a virtual machine or JIT;
- transpiling backends;
- sandboxed embedded runtimes;
- specialized platform backends.
A backend may choose a different execution strategy, but it must not silently redefine language behavior.
Each .pop source file is a Module inside one file-scoped namespace.
namespace Game.PlayersDeclarations have explicit visibility:
publicis available to dependent libraries;internalis available inside the same independently compiled unit;privateis limited to the current file.
Omitted namespace-level visibility defaults to internal.
Projects use bubble.toml for package metadata and dependencies. Pop's ownership model is:
Item → Module → Bubble → Package → Workspace
A Bubble is an independently compiled dependency and the boundary for internal visibility. A Package is a versioned, publishable directory. A Workspace groups packages under one resolver, lockfile, cache, and policy root.
A conventional project layout is:
bubble.toml
src/
lib.pop
main.pop
bin/
tests/
examples/
benchmarks/
Modules and namespaces are compile-time concepts. They are not values returned from require, and dependency loading cannot be computed from arbitrary runtime strings.
Pop is designed around one coherent toolchain for common development tasks:
pop check
pop build
pop run
pop test
pop format
pop lint
pop add
pop remove
Diagnostics, symbols, metadata, documentation, and automated fixes are intended to be available through stable structured formats, so editors and build tools do not need to scrape terminal output.
Human CLI and compiler diagnostics are available in English, Simplified
Chinese, Japanese, Brazilian Portuguese, and Spanish. Select a language for one
invocation with pop --language pt-BR check source.pop, set POP_LANGUAGE, or
write a user configuration file:
# $XDG_CONFIG_HOME/pop/config.toml or $HOME/.config/pop/config.toml
language = "pt-BR"The supported tags are en, zh-Hans, ja, pt-BR, and es. An explicit
option takes precedence over the environment and user configuration. Machine
schemas, diagnostic codes, identifiers, paths, and HIR/MIR/LLVM dumps remain
language-independent.
The language formatter owns canonical whitespace and layout. Pop source uses four-space indentation, avoids mandatory semicolons, and aims for one obvious readable shape.
Pop prefers familiar Luau syntax whenever the semantics truly match. Familiarity does not override the language model.
Examples of the current direction:
| Luau area | Pop direction |
|---|---|
| Local inference | Adopt, without gradual fallback |
| Type annotations and narrowing | Adopt and strengthen |
| First-class functions and closures | Adopt with native typed environments |
| Coroutines | Adapt to backend-neutral runtime operations |
| Arrays and table literals | Adopt the ergonomics with static element types |
| Tables as records or objects | Replace with records, classes, interfaces, arrays, and typed tables |
| Metatables and metamethods | Replace common cases with native constructs and typed protocols |
require-style module values |
Replace with namespaces, package dependencies, and analyzable initialization |
| Implicit globals | Reject |
One universal number type |
Replace with explicit integer and floating-point types |
any and dynamic escape behavior |
Reject |
| Sandboxed embedding | Preserve as a deployment capability |
The full feature inventory is versioned because Luau continues to evolve. A feature is not silently forgotten: it must be adopted, adapted, replaced, rejected with a reason, or deliberately deferred.
- Keep the common case small and readable.
- Prefer data and functions before classes and inheritance.
- Preserve Luau-shaped syntax when its meaning remains accurate.
- Give every usable value and operation a static type.
- Keep dynamic or foreign data behind explicit typed boundaries.
- Make modules, packages, and visibility analyzable before execution.
- Keep runtime behavior consistent across conforming backends.
- Do not reintroduce Lua's table-centered semantics through compatibility shortcuts.
Pop Lang is under active development and is not yet a stable production language.
The repository currently contains the language design, accepted architecture decisions, compiler and runtime foundations, tooling work, and conformance tests. Syntax and runtime behavior may still change as implementation milestones are completed.
The architecture documents are the current source of truth:
architecture/ Language, runtime, compiler, and tooling design
architecture/decisions/
Accepted Architecture Decision Records
crates/compiler/ Syntax, resolution, types, compile time, HIR, MIR, drivers
crates/extensions/ Independent Pop.Data/Ai/Cli/Rpc/Syntax/Lsp package builds
crates/libraries/ Modular Pop.Internal/Pop.Standard Rust bootstrap code
plus conventionally discovered pop/ source roots
crates/runtime/ PLRI and bootstrap/native runtime contracts
crates/tools/ Architecture tests, formatter, documentation, CLI tooling
libraries/internal/ Pop.Internal verified bootstrap metadata
libraries/standard/ Pop.Standard verified bootstrap metadata
The compiler and runtime are implemented in Rust. Rust is the implementation language of the toolchain; it does not define Pop source syntax or its package model.
The foundation-library contributor guide shows how ordinary portable Math and Sequence work stays in focused Pop Modules, how the remaining Text prototype is isolated, and when an intrinsic or native-boundary change genuinely needs compiler or runtime knowledge.
Copyright (C) 2026 Julia Klee.
The compiler, backends, tools, and default repository material are licensed under GPL version 3 only. Runtime components, foundational libraries, official extensions, examples, and project-owned code copied or linked into user applications are licensed under Apache License 2.0. Using Pop Lang does not make a user's own source or ordinary compiler output GPL-covered.
See LICENSE.txt for the complete path map and references to the GNU and Apache license texts stored in each crate family.
