Appearance
Type Functions RFCs
Written: 2026-10-14
Learning objectives
- You define small user type functions.
- You read and build table, function, and generic types.
- You index tables and list their keys in types.
- You query subtyping inside type functions.
- You know type functions need the new solver.
Prerequisites
User-defined type functions
They compute types from types. You use them for reusable type-level helpers.
Original problem
The type system could not express relations between types. Helpers that made every field optional or picked a field type needed any casts or reshaped APIs. Libraries lost precision at exactly the shared boundaries readers trust most.
Change and motivation
You write type functions in ordinary Luau with a type function header. Parameters arrive as type values. You inspect them with the type API and return one type. Calls use angle brackets at type positions. The user type functions RFC runs them in a sandbox during analysis with no runtime cost.
Implemented semantics
You declare with type function name in parentheses and end. The body sees only the sandbox libraries plus the types library. You return exactly one type. Errors and asserts become analysis errors at the call. Type aliases and type functions share scoping rules, so they reference each other freely.
Unlike a normal function in two ways, it runs at analysis time and takes and returns types. Unlike a built-in operator, you define it yourself.
Type functions need the new solver. You enable it for the file or project before you use this chapter. The old solver does not reduce user type functions.
Before and after
Before:
luau
--!strict
type MaybeName = string?
local nick: MaybeName = nil
print(nick)txt
nilEvery optional needs hand-written syntax. Helpers cannot build the shape for you.
After:
luau
--!strict
type function Optional(t)
return types.optional(t)
end
type MaybeName = Optional<string>
local nick: MaybeName = nil
print(nick)txt
nilThe helper wraps any type in an optional through the verified library call.
Realistic example
You offer a type helper that makes a receipt id optional-safe for order records.
luau
--!strict
type function Optional(t)
return types.optional(t)
end
type Order = { id: number, note: Optional<string> }
local first: Order = { id = 7, note = nil }
print(first.id)txt
7You define the one-line helper. You use it inside a table type. You build a record with a nil note. The checker accepts it because the field is optional.
Strict implications
Type function bodies are dynamically typed Luau in a sandbox. The surrounding annotations still check strictly. A type function that errors on bad input gives callers precise diagnostics, so you validate tags with is and raise clear errors.
Edge cases
You return one type, never a pack. Extra return values error. The sandbox lacks require and game access, so you compute from the arguments alone. Long-running type functions can hit analysis limits, so you keep helpers small.
Status
The user-defined type functions RFC shows Status: Implemented. You use it today under the new solver. You still confirm it in Studio per how to read status.
Generics in type functions
Generic values let type functions build and inspect generic function types. You use them for higher-order helpers.
Original problem
Type functions rejected any type containing generics. Real code is full of generic functions, so even a helper that ignored generics failed at serialization. Classes from the environment failed the same way.
Change and motivation
The types library gained a generic constructor plus getters and setters on function types. You create named type and pack generics, attach them to built function types, and read them back. The generics in type functions RFC keeps lexical scoping, so reused names resolve per scope.
Implemented semantics
You call types dot generic with a name and an optional pack flag. You build a function type with newfunction. You set parameters, returns, and the generic list. Names define identity, so two generics with one name in one scope are equal. Ill-scoped uses error at serialization.
Unlike plain types, generics stand for unknown types per use. Unlike syntax generics, you construct them with calls.
Before and after
Before:
luau
--!strict
type function Optional(t)
return types.optional(t)
end
type MaybeId = Optional<number>
local id: MaybeId = 3
print(id)txt
3Plain helpers cover concrete types. Generic function types need the new constructor.
After:
luau
--!strict
type function Identity()
local T = types.generic("T")
local f = types.newfunction()
f:setparameters({ head = { T } })
f:setreturns({ head = { T } })
f:setgenerics({ T })
return f
end
type IdentityFn = Identity<>
local run: IdentityFn = function<T>(value: T): T
return value
end
print(run(12))txt
12You build the generic identity shape step by step. The alias names it. An ordinary generic function fits it.
Realistic example
You describe a mapper helper that lifts any element function into service code.
luau
--!strict
type function Mapper()
local T = types.generic("T")
local f = types.newfunction()
f:setparameters({ head = { T } })
f:setreturns({ head = { T } })
f:setgenerics({ T })
return f
end
type MapperFn = Mapper<>
local double: MapperFn = function<T>(value: T): T
return value
end
print(double("sword"))txt
swordYou construct the generic function type. You implement it once. You run it with a string. The generic flows through untouched.
Strict implications
Constructed generics check like written ones at the use. A caller passing the wrong value still errors. You keep generic lists ordered with types before packs, because a type after a pack is invalid.
Edge cases
Reusing one name in nested function types shadows per scope, matching source syntax. Duplicate names in one list error. Holding a pack generic as a standalone value is allowed in the runtime even though no syntax spells it, so you only return it inside proper positions.
Status
The generics in type functions RFC shows Status: Implemented. You use it today under the new solver. You still confirm it in Studio per how to read status.
Thread and buffer in type functions
They work in type functions now. You use them when helpers touch async or binary data.
Original problem
The original type function API omitted two primitive types. Helpers that met a thread or buffer value had no constant to return or compare. Developers worked around the gap with errors or any.
Change and motivation
The library gained thread and buffer constants, and tags accept both names. No new methods were needed. The thread and buffer RFC closes the gap with two properties.
Implemented semantics
You read types dot thread and types dot buffer like the number and string constants. You test tags with is using the same names. Both behave as immutable primitive instances.
Unlike tables and functions, no properties or methods hang off these tags. Unlike the old API, the gap is gone.
Before and after
Before:
luau
--!strict
type function IsText(t)
if t:is("string") then
return types.singleton(true)
end
return types.singleton(false)
end
type TextCheck = IsText<string>
local yes: TextCheck = true
print(yes)txt
trueHelpers distinguished strings. Threads and buffers had no matching constants.
After:
luau
--!strict
type function IsBuffer(t)
if t:is("buffer") then
return types.singleton(true)
end
return types.singleton(false)
end
type BufferCheck = IsBuffer<buffer>
local yes: BufferCheck = true
print(yes)txt
trueThe same shape now recognizes buffers. The tag test and singleton answer are both verified API.
Realistic example
You tag incoming channel values so packet code branches on binary payloads.
luau
--!strict
type function IsBuffer(t)
if t:is("buffer") then
return types.singleton(true)
end
return types.singleton(false)
end
type PacketFlag = IsBuffer<buffer>
local binary: PacketFlag = true
print(binary)txt
trueYou test the tag. You answer with a singleton boolean. The alias carries the verdict into annotations.
Strict implications
Tag tests refine nothing by themselves outside type functions. Inside helpers they route logic cleanly. Callers still see precise singletons, so downstream narrowing works as usual.
Edge cases
Thread and buffer have no properties method, so you never call properties on them. You return the constants when you need the primitive itself rather than a verdict about it.
Status
The thread and buffer RFC shows Status: Implemented. You use it today under the new solver. You still confirm it in Studio per how to read status.
Index operator
It reads a property type from another type. You use it to stay in sync with shared records.
Original problem
You reached a field type by declaring a throwaway variable and wrapping it in typeof. That trick added runtime code for a type-level question. Growing records forced you to revisit every copy.
Change and motivation
The built-in index function takes a type and a key type and reduces to the field type. Unions distribute across members. Metatable index chains resolve when the direct field is missing. The index RFC removes the throwaway variable.
Implemented semantics
You write index in angle brackets with the target type and the key singleton. Missing keys error. Key unions produce unions of field types. A union target distributes per member. Metatable lookup runs only after the direct search fails, with a bounded chain.
Unlike typeof indexing, no value is needed. Unlike raw access, metamethods participate.
Before and after
Before:
luau
--!strict
type Pet = { name: string, level: number }
local sample = { name = "Pip", level = 3 }
type Level = typeof(sample.level)
local stage: Level = 4
print(stage)txt
4The sample exists only to name the field type. Deleting it breaks the alias.
After:
luau
--!strict
type Pet = { name: string, level: number }
type Level = index<Pet, "level">
local stage: Level = 4
print(stage)txt
4The alias reads the field directly from the type. No value is involved. Record growth elsewhere never touches this line.
Realistic example
You derive a reward type from the canonical quest record.
luau
--!strict
type Quest = { title: string, reward: number, zone: string }
type Reward = index<Quest, "reward">
local function grant(amount: Reward): string
return `Granted {amount} coins.`
end
print(grant(50))txt
Granted 50 coins.You define the quest once. You derive the reward. The granter takes exactly that. Changing the record updates the parameter automatically.
Strict implications
Derived types check exactly like written ones. A renamed field errors at the index use, which points you at the real change. You keep keys as singletons, since a general string cannot select one field.
Edge cases
An unknown key errors rather than widening to unknown. Inexact tables can surprise authors who expect silent widening, so you treat a missing key as a bug first. Deep metatable chains give up after a bound, so extremely layered lookups can fail to reduce.
Status
The index RFC shows Status: Implemented. You use it today under the new solver. You still confirm it in Studio per how to read status.
Keyof operators
It lists the keys of a table type. You use it for registries and selectors.
Original problem
You hand-wrote a union of key names beside every registry table. Each new entry needed two edits in two places. Skipping the second edit silently widened the selector to string or broke safety.
Change and motivation
Keyof computes the key union from the table type. Rawkeyof computes only the direct keys and skips metatable contributions. Union targets reduce to the shared keys. The keyof RFC keeps one source of truth.
Implemented semantics
You write keyof in brackets around the table type. The result is a union of key singletons. Index-style lookup includes metatable keys, while rawkeyof excludes them. Union inputs yield the greatest common key subset.
Unlike a hand union, it follows the table automatically. Between the two operators, metatable keys count for keyof only.
Before and after
Before:
luau
--!strict
local beasts = { cat = 1, dog = 2 }
type Beast = "cat" | "dog"
local function count(kind: Beast): number
return beasts[kind]
end
print(count("dog"))txt
2The union duplicates the keys. Adding a beast means editing both lines.
After:
luau
--!strict
local beasts = { cat = 1, dog = 2 }
type Beast = keyof<typeof(beasts)>
local function count(kind: Beast): number
return beasts[kind]
end
print(count("dog"))txt
2The alias derives from the value type. New keys flow in with no second edit. Bad keys still error.
Realistic example
You guard a zone registry so teleport code only names real zones.
luau
--!strict
local zones = { forest = 1, arena = 2, harbor = 3 }
type Zone = keyof<typeof(zones)>
local function travel(to: Zone): string
return `Traveling to {to}.`
end
print(travel("arena"))txt
Traveling to arena.You list zones once. You derive the selector. Travel accepts only known names. A typo fails before playtest.
Strict implications
Selectors stay precise without maintenance. Indexing with a keyof value is safe because every member exists. You still handle the value behind the key, which may be optional or narrow.
Edge cases
Union inputs keep only shared keys, which can collapse to never for disjoint shapes. Metatable-heavy classes need a conscious choice between keyof and rawkeyof. Overly wide tables produce wide key unions, so you keep registries as precise literals.
Status
The keyof RFC shows Status: Implemented. You use it today under the new solver. You still confirm it in Studio per how to read status.
Rawget operator
It reads a direct field type without metamethods. You use it for precise override checks.
Original problem
Index-style reads followed metatables. Code that needed the direct answer had no type-level spelling. Runtime rawget and its type story disagreed.
Change and motivation
The built-in rawget function mirrors the runtime operator. It reads only direct fields. Missing direct fields error. Classes have no direct fields in this sense, so they error. The rawget RFC reuses the index machinery with metatables disabled.
Implemented semantics
You write rawget in brackets with the table type and key. Direct fields reduce normally. Inherited fields error instead of resolving. Error wording matches the index operator.
Unlike index, metatables never participate. Unlike rawkeyof, it reads one value rather than listing keys.
Before and after
Before:
luau
--!strict
type Base = { title: string }
type Title = index<Base, "title">
local name: Title = "Herbs"
print(name)txt
HerbsPlain reads suffice for direct tables. Metatable layers motivate the raw form.
After:
luau
--!strict
type Base = { title: string }
type Title = rawget<Base, "title">
local name: Title = "Herbs"
print(name)txt
HerbsThe raw read names the direct field. Same result here, stronger promise under metatables.
Realistic example
You verify that a skin override actually defines its own art id.
luau
--!strict
type Skin = { art: string }
type Art = rawget<Skin, "art">
local function paint(id: Art): string
return `Paint {id}.`
end
print(paint("gold"))txt
Paint gold.You require the direct field. The painter takes exactly that. Inherited art never satisfies this alias.
Strict implications
You choose rawget when override presence matters. Callers must supply the field directly. You choose index when inheritance should count. The checker enforces your choice at the alias.
Edge cases
Any metatable-provided field errors under rawget by design. Class types error because they expose no direct fields. You do not wrap the call in optionals to soften it; you switch to index when softness is correct.
Status
The rawget RFC shows Status: Implemented. You use it today under the new solver. You still confirm it in Studio per how to read status.
Metatable type functions
They attach and read metatables in types. You use them for OOP patterns without values.
Original problem
You applied metatable information only through typeof around a runtime call. That forced a value into existence for a type-level fact. Common class patterns carried boilerplate at every definition.
Change and motivation
Setmetatable takes a table type and a metatable type and returns the combined type. Getmetatable reads the metatable back, respects the locked metatable field, and returns nil when none is present. Unions and intersections map per member. The metatable functions RFC mirrors the runtime pair.
Implemented semantics
You write setmetatable in brackets with both table types. Non-table first arguments error, including classes. A non-table second argument errors. Getmetatable on an unsupported type errors. Locked metatables surface their guard value.
Unlike the runtime call, nothing mutates and a new type is produced. Unlike manual annotation, the combination stays machine-checked.
Before and after
Before:
luau
--!strict
local base = { title = "scout" }
type Rank = typeof(setmetatable({ title = "scout" }, { __index = base }))
local rank: Rank = setmetatable({ title = "lead" }, { __index = base })
print(rank.title)txt
leadThe alias needs a runtime expression inside typeof. Readers parse values to learn types.
After:
luau
--!strict
local base = { title = "scout" }
type Rank = setmetatable<{ title: string }, { __index: typeof(base) }>
local rank: Rank = setmetatable({ title = "lead" }, { __index = base })
print(rank.title)txt
leadThe alias combines two types directly. No extra value exists. The construction below still matches it.
Realistic example
You type a pet class table and read its backing table type back.
luau
--!strict
local PetMethods = { speak = "woof" }
type Pet = setmetatable<{ name: string }, { __index: typeof(PetMethods) }>
type Backing = getmetatable<Pet>
local pup: Pet = setmetatable({ name = "Pip" }, { __index = PetMethods })
print(pup.name)txt
PipYou attach the method table as a type. You recover the metatable as a type. The instance construction matches the alias. The Backing alias documents the round trip.
Strict implications
Combined types check method access through the metatable. Direct field writes still follow table rules. You annotate constructors with the combined alias so callers see methods immediately.
Edge cases
The set name suggests mutation, but no mutation happens. Applying a metatable over a union maps per member, which can widen faster than you expect. A locked metatable field changes what getmetatable returns, so you test that path explicitly.
Status
The metatable type functions RFC shows Status: Implemented. You use them today under the new solver. You still confirm them in Studio per how to read status.
Type issubtypeof
It asks the subtyping question inside a helper. You use it for type predicates.
Original problem
Helpers hand-rolled subtype checks with tag tests and component loops. String checks needed singleton unwrapping. Enum checks looped unions. Table checks ballooned into partial algorithms that risked timeouts.
Change and motivation
Type values gained an issubtypeof method that runs the real subtyping algorithm. You call it with the candidate supertype and receive a boolean. The issubtypeof RFC deletes the hand-rolled versions.
Implemented semantics
You write target colon issubtypeof of candidate inside a type function. The call returns a boolean in the sandbox. You branch on it or wrap it in a singleton for the type level. No new syntax is involved.
Unlike tag equality, it answers the semantic question, not the syntactic one. Unlike manual loops, it terminates through the real algorithm.
Before and after
Before:
luau
--!strict
type function IsText(t)
if t:is("string") then
return types.singleton(true)
end
return types.singleton(false)
end
type TextCheck = IsText<string>
local yes: TextCheck = true
print(yes)txt
trueTag tests answer one question. Relationships need more.
After:
luau
--!strict
type function IsText(t)
if t:issubtypeof(types.string) then
return types.singleton(true)
end
return types.singleton(false)
end
type TextCheck = IsText<string>
local yes: TextCheck = true
print(yes)txt
trueThe method asks the checker directly. Singletons like literal strings now pass as text. The answer stays a singleton boolean.
Realistic example
You accept any coin-like value, including singletons, in a shop helper.
luau
--!strict
type function IsText(t)
if t:issubtypeof(types.string) then
return types.singleton(true)
end
return types.singleton(false)
end
type NameCheck = IsText<"Maya">
local named: NameCheck = true
print(named)txt
trueA literal singleton is a subtype of string. Tag equality would miss it. The method catches it.
Strict implications
Predicates built this way stay honest at the type level. Callers branch on singleton true or false with normal narrowing. You keep the boolean inside the sandbox; only the singleton crosses into types.
Edge cases
The method answers subtyping, not assignability through any suppression, so you still design boundaries with unknown where input is untrusted. Overusing it in wide unions can cost analysis time, so you short circuit on tags first for the obvious cases.
Status
The issubtypeof RFC shows Status: Implemented. You use it today under the new solver. You still confirm it in Studio per how to read status.
Types optional
It wraps any type in an optional. You use it inside helpers that model missing data.
Original problem
Every helper that needed an optional spelled the union with a nil singleton by hand. Codebases copied a tiny local helper into each module. Readers tripped over the repetition.
Change and motivation
The types library gained an optional constructor. Unions gain nil through it. Other types wrap the same way. The optional RFC standardizes the one-liner.
Implemented semantics
You call types dot optional with one type. Union inputs gain nil among their members. Non-union inputs become a two-member union with nil. Behavior matches the hand-written union exactly.
Unlike the manual spelling, it is shorter. Unlike any, it stays precise.
Before and after
Before:
luau
--!strict
type function Optional(t)
return types.unionof(t, types.singleton(nil))
end
type MaybeZone = Optional<string>
local zone: MaybeZone = nil
print(zone)txt
nilThe manual union works. Every helper repeats it.
After:
luau
--!strict
type function Optional(t)
return types.optional(t)
end
type MaybeZone = Optional<string>
local zone: MaybeZone = nil
print(zone)txt
nilThe library call replaces two calls with one. The meaning is identical.
Realistic example
You model an optional subtitle for quest banners.
luau
--!strict
type function Optional(t)
return types.optional(t)
end
type Banner = { title: string, sub: Optional<string> }
local plain: Banner = { title = "Herbs", sub = nil }
print(plain.title)txt
HerbsThe title is required. The subtitle is optional through the helper. Nil passes cleanly.
Strict implications
Wrapped fields read as optionals everywhere downstream. You narrow them before use. The helper never hides nil from the checker, which is the point.
Edge cases
Calling it on a union that already holds nil is harmless. Calling it twice nests no deeper than once in meaning. You still prefer the question-mark shorthand in plain annotations and reserve the call for type function bodies.
Status
The optional RFC shows Status: Implemented. You use it today under the new solver. You still confirm it in Studio per how to read status.
Extern tag
It renames the foreign-type tag. You use it when helpers meet Engine instances.
Original problem
Foreign types arrived from C and C++ embedders. Internals first called them class types, which collided with the planned classes feature and confused authors. The user-facing tag kept the old name after internals moved on.
Change and motivation
The tag value becomes extern. Tag tests use the new name. Documentation drops the class wording for foreign data. The extern tag RFC fixes the inconsistency before the new solver leaves beta.
Implemented semantics
You test type colon is with extern for foreign values. The old class spelling no longer matches. Tags for tables, functions, and primitives are unchanged. Engine instances like Player surface under the new tag.
Unlike the classes proposal, extern describes foreign data, never user classes. Unlike a new capability, only the name changes.
Before and after
Before:
luau
--!strict
type function IsForeign(t)
if t:is("extern") then
return types.singleton(true)
end
return types.singleton(false)
end
type ForeignCheck = IsForeign<Player>
local yes: ForeignCheck = true
print(yes)txt
trueThe new spelling recognizes a Player as foreign. The old class spelling belongs to beta history.
After:
luau
--!strict
type function IsForeign(t)
if t:is("extern") then
return types.singleton(true)
end
return types.singleton(false)
end
type TableCheck = IsForeign<{ name: string }>
local no: TableCheck = false
print(no)txt
falsePlain tables are not foreign. The same test answers false.
Realistic example
You route foreign handles and plain records through different helpers.
luau
--!strict
type function IsForeign(t)
if t:is("extern") then
return types.singleton(true)
end
return types.singleton(false)
end
type HandleCheck = IsForeign<Player>
local handle: HandleCheck = true
print(handle)txt
trueA Player tests foreign. A record would not. Downstream code picks the right path from the singleton.
Strict implications
You never confuse extern with user OOP. Roblox classes come from the Engine, so they test extern. Your metatable classes stay tables. The checker keeps those worlds apart.
Edge cases
Beta-era code testing class needs an update to extern. The classes proposal has no Implemented line, so class in that sense never arrives here. You search helpers for the old spelling after enabling the new solver.
Status
The extern tag RFC shows Status: Implemented. You use it today under the new solver. You still confirm it in Studio per how to read status.
Common mistakes
Wrong: you return two types from a helper.
luau
--!strict
type function Optional(t)
return types.optional(t)
end
type MaybeGem = Optional<string>
local gem: MaybeGem = "ruby"
print(gem)The single return is correct. The mistake is adding a second return beside it. Fix it by returning exactly one type.
Wrong: you test foreign values with the beta spelling.
luau
--!strict
type function IsForeign(t)
if t:is("extern") then
return types.singleton(true)
end
return types.singleton(false)
end
type HandleCheck = IsForeign<Player>
local handle: HandleCheck = true
print(handle)The extern spelling is correct. The mistake is the old class string. Fix it by searching helpers for the old name.
Wrong: you expect type functions on the old solver.
luau
--!strict
type Pet = { name: string, level: number }
type Level = index<Pet, "level">
local stage: Level = 2
print(stage)This needs the new solver. The mistake is enabling nothing and reading a confusing error. Fix it by switching the file or project to the new solver first.
Wrong: you use index where rawget is meant.
luau
--!strict
type Base = { title: string }
type Title = rawget<Base, "title">
local name: Title = "Herbs"
print(name)The raw read is correct for direct-only promises. The mistake is reaching for index when inheritance must not count. Fix it by choosing per question asked.
Exercises
- Write a one-line optional helper and use it.
- Derive a field type with index.
- Derive a key union with keyof.
- Attach a metatable in a type and read it back.
- Answer a subtype question with issubtypeof.
- Test a Player for the extern tag.
Solution 1
Run this pure snippet in the Command Bar.
luau
--!strict
type function Optional(t)
return types.optional(t)
end
type MaybePet = Optional<string>
local pet: MaybePet = nil
print(pet)txt
nilYou wrap string in an optional. Nil passes. The helper stays one line.
Solution 2
Run this pure snippet in the Command Bar.
luau
--!strict
type Quest = { title: string, reward: number }
type Reward = index<Quest, "reward">
local pay: Reward = 25
print(pay)txt
25You read the reward type from the record. The local takes exactly that. No sample value exists.
Solution 3
Run this pure snippet in the Command Bar.
luau
--!strict
local shop = { sword = 150, bow = 120 }
type Item = keyof<typeof(shop)>
local pick: Item = "bow"
print(shop[pick])txt
120You derive the key union from the shop. The pick must name a real item. Indexing stays safe.
Solution 4
Run this pure snippet in the Command Bar.
luau
--!strict
local Methods = { level = 1 }
type Mob = setmetatable<{ name: string }, { __index: typeof(Methods) }>
local one: Mob = setmetatable({ name = "Slime" }, { __index = Methods })
print(one.name)txt
SlimeYou combine the data type with the method table. The construction matches. The name prints.
Solution 5
Run this pure snippet in the Command Bar.
luau
--!strict
type function IsText(t)
if t:issubtypeof(types.string) then
return types.singleton(true)
end
return types.singleton(false)
end
type Check = IsText<"scout">
local yes: Check = true
print(yes)txt
trueThe singleton passes the subtype test. Tag equality alone would miss it. The alias carries true.
Solution 6
Run this pure snippet in the Command Bar.
luau
--!strict
type function IsForeign(t)
if t:is("extern") then
return types.singleton(true)
end
return types.singleton(false)
end
type HandleCheck = IsForeign<Player>
local handle: HandleCheck = true
print(handle)txt
trueA Player tests extern. The helper answers with a singleton. The annotation holds true.
Summary
- User helpers take and return types. They run under the new solver.
- Generics, thread, and buffer all work inside helpers.
- Index, keyof, and rawget derive types from shapes.
- Metatable operators combine and recover types.
- Issubtypeof and optional shorten predicates and unions.
- Extern names foreign data.
- Next: Stdlib and Config.