Early preview: syntax, standard library APIs, and tooling may change.

Core DataUnions

Unions

Use unions to describe a closed set of states, with or without associated data.

Payload-free Unions

Variants without payloads form a closed set of labels. The compiler rejects unknown labels, and exhaustive matching makes each case explicit. Adding a variant identifies the matches that need updating.

union Direction:
	North
	East
	South
	West


pure func direction_name(direction: Direction) -> String:
	match direction:
		North: "north"
		East: "east"
		South: "south"
		West: "west"


func main(args: List[String]) -> Void:
	print(direction_name(North)) -- north
	print(direction_name(West)) -- west

Data-carrying Unions

Variants can carry different data: Running(Int) holds progress, while Done(String) holds a path. A value has exactly one variant, so each state carries only the data that belongs to it. Match the variant to access its payload.

union Download:
	Queued
	Running(Int)
	Done(String)
	Failed(String)


func main(args: List[String]) -> Void:
	status: Download = Running(40)
	match status:
		Queued: print("queued")
		Running(percent): print("${percent}% complete") -- prints: 40% complete
		Done(path): print("saved ${path}")
		Failed(message): print("failed: ${message}")

Generic Unions

A union can take type parameters. In Lookup[T], Found carries a value of type T, while Missing carries no data. Lookup[Int] and Lookup[String] share the same structure with different payload types. The generic value_or function works with either, keeping the fallback and result the same type as the payload.

union Lookup[T]:
	Missing
	Found(T)


pure func value_or[T](lookup: Lookup[T], fallback: T) -> T:
	match lookup:
		Missing: fallback
		Found(value): value


func main(args: List[String]) -> Void:
	score: Lookup[Int] = Found(42)
	name: Lookup[String] = Found("Ada")
	absent: Lookup[String] = Missing
	print(value_or(score, 0).to_string()) -- prints: 42
	print(value_or(name, "guest")) -- prints: Ada
	print(value_or(absent, "guest")) -- prints: guest

Recursive Unions

A union can refer to itself in a variant's payload. Here an Expression is either a number or the sum of two more expressions, allowing trees of arbitrary depth. Use a non-recursive variant to end each branch.

union Expression:
	Number(Int)
	Add(Expression, Expression)


pure func evaluate(expression: Expression) -> Int:
	match expression:
		Number(value): value
		Add(left, right): evaluate(left) + evaluate(right)


func main(args: List[String]) -> Void:
	expression: Expression = Add(Number(2), Add(Number(3), Number(4)))
	print(evaluate(expression).to_string()) -- prints: 9

Fixed Unions

fixed union makes allocation-free union storage a compiler-checked contract. Constructing, copying, passing, and returning an eligible value require no separate union allocation or hidden payload box. Fixed and non-fixed unions receive the same optimizations for identical bodies; fixed requires the guarantee, rather than selecting a different optimizer.

fixed union Command:
	Idle
	Move(Int)


pure func distance(command: Command) -> Int:
	match command:
		Idle: 0
		Move(steps): steps


func main(args: List[String]) -> Void:
	print(distance(Idle).to_string()) -- prints: 0
	print(distance(Move(12)).to_string()) -- prints: 12

Fixed Eligibility

Payload-free variants and scalar payloads such as Int, Float, and Bool are eligible. Every variant and nested payload must satisfy the fixed-storage rules, even if never constructed. Nested records and unions must also be fixed. Non-fixed records and unions are ineligible even when all their fields are scalar. Direct or nested String payloads are ineligible, even when the string already exists. Directly recursive unions such as Expression are ineligible because they have no finite inline layout. Use non-fixed union for these cases. A fixed union need not live on the stack: it can live inside an already allocated container, and growing that container may still allocate.

Example

download.brp
union Download:
	Queued
	Running(Int)
	Done(String)
	Failed(String)


pure func describe(download: Download) -> String:
	match download:
		Queued: "queued"
		Running(percent): "running " + percent.to_string()
		Done(path): "done: " + path
		Failed(msg): "failed: " + msg


func main(args: List[String]) -> Void:
	print(describe(Running(40))) -- prints: running 40

Try it

terminal
blorp run download.brp