A domain-specific language for Roblox that transpiles to Luau.
⚠️ This project is a work-in-progress. Nothing is final. Breaking changes may occur at any time.
- Immutability by default – Variables, fields, and arrays are immutable unless explicitly marked
mut - Structural type system – Duck typing with compile-time safety
- Modern syntax – Familiar syntax inspired by Rust and TypeScript
- Rich type inference – Minimal annotations required
- Extended number literals – Automatic math for units of time and frequency, as well as binary/octal/hex support
- Range expressions –
1..10for slicing and bounds nameofoperator – Get names as strings at compile time- Generic functions and types – Full support for type parameters including constraints and defaults
- Indices starting at one – Same as Luau for familiarity
- Zero-cost abstractions – Transpiles to idiomatic Luau with minimal overhead
typeofx in collection- Implementors for interfaces
- Private visibility for interface fields & methods
- Event declarations
- Full module system (imports/exports)
- Error handling using the result pattern
- Roblox type generator + Luau typings
let x: bool = false;const x: boolean = falsemut x = 1;local x = 1let s = "abc" + "def";local s = "abc" .. "def"let x = 1 & 2 & 3;local x = bit32.band(1, 2, 3)type Union<A, B> = A | B;
let x: Union<bool, string> = false;type Union<A, B> = A | B
const x: Union<boolean, string> = falseLoom supports extended number literals that let you do boilerplate math to convert to a specific unit instantaneously.
let a = 10s;
let b = 100ms;
let c = 10m;
let d = 1h;
let e = 16hz;
let f = 100_000_000
let hex = 0xF00D;
let binary = 0b11001;
let octal = 0o400;const a = 10
const b = 0.1
const c = 600
const d = 3600
const e = 0.0625
const f = 100000000
const hex = 61453
const binary = 25
const octal = 256mut x = 69;
x = 420;local x = 69
x = 420mut x = 69;
mut y = 420;
let z = x = y = 1;local x = 69
local y = 420
y = 1
x = y
const z = xLoom supports shorthand function bodies that return single expressions.
fn one -> 1;const function one()
return 1
endfn id<T>(value: T) -> value;const function id<T>(value: T)
return value
endfn id<T: number>(value: T): T {
return value;
}
id::<number>(69)const function id<T>(value: T & number): T & number
return value
end
id(69)let arr: number[] = [1, 2, 3];const arr: { number } = {1, 2, 3}Arrays are immutable by default, but can be declared as mutable.
let arr: number[mut] = mut [1, 2, 3];const arr: { number } = {1, 2, 3}Assignments are expressions in loom.
let arr = mut [1, 2, 3];
let x = arr[1] = 69;const arr: { number } = {1, 2, 3}
const x = 69
arr[1] = xThe nameof operator can be used to read the tokens of Name expressions as a string.
let abc = 69;
let name = nameof(abc)const abc = 69;
const name = "abc"Ranges are constructs that represent a minimum and a maximum number.
let range = 1..10;const range = { minimum = 1, maximum = 10 }They can be used to slice arrays.
let range = 1..3;
let arr = [1, 2, 3, 4, 5];
let slice = arr[range];const range = { minimum = 1, maximum = 3 }
const arr = {1, 2, 3, 4, 5}
const _length = #arr
const slice = table.move(arr, math.clamp(range.minimum, 1, _length), math.clamp(range.maximum, 1, _length), 1, {})let arr = [1, 2, 3, 4, 5];
let slice = arr[1..3];const arr = {1, 2, 3, 4, 5}
const _length = #arr
const slice = table.move(arr, math.clamp(1, 1, _length), math.clamp(3, 1, _length), 1, {})As well as strings.
let s = "abcdef";
let slice = s[1..3];const s = "abcdef"
const slice = string.sub(s, 1, 3)let s = "abcdef";
let char = s[1];const s = "abcdef"
const char = string.sub(s, 1, 1)let min = (1..10).minimum;const min = ({ minimum = 1, maximum = 10 }).minimumlet range = 1..10;
let name = nameof(range.minimum);const range = { minimum = 1, maximum = 10 }
const name = "range.minimum"Enums are named compile-time constants.
enum Abc { A, B = 69, C }
let a = Abc.A;
let b = Abc.B;
let c = Abc.C;type Abc = number
const a = 0
const b = 69
const c = 70They can also be used with strings.
enum Tag: string {
Lava = "lava",
Something = "something"
}
let tag = Tag.Lavatype Tag = "lava" | "something"
const tag = "lava"if 69 == 420 {
let foo = 69
} else if 69 == 69 {
let yes = "yes"
}if 69 == 420 then
const foo = 69
elseif 69 == 69 then
const yes = "yes"
endDeclare statements allow you to declare types for symbols that may not exist in your file but you know exist in your environment.
declare fn print(msg: unknown): void;
print("hello, world!");print("hello, world!")declare let x: number;
let y = x + 1;const y = x + 1let unknown = 69 as unknown;const unknown = (69 :: unknown)type Callback = fn(): voidtype Callback = () -> ()interface HasName {
name: string;
}
interface HasAge {
age: number;
}
interface Person: HasName, HasAge {
job: string;
}type HasName = {
read name: string;
}
type HasAge = {
read age: number;
}
type Person = HasName & HasAge & {
read job: string;
}interface ImmutRecord<K, V> {
[K]: V;
}type ImmutRecord<K, V> = { read [K]: V }In this example S resolves to string.
interface Foo { bar: string }
type S = Foo["bar"];type Foo = {
read bar: string
}
type S = index<Foo, "bar">interface Person {
name: string;
mut age: number;
}
let runic = new Person { name: "Runic", age: 21 };
runic.age = 69;type Person = {
read name: string,
age: number
}
const runic = { name = "Runic", age = 21 }
runic.age = 69mut i = 0;
while i < 10
i += 1;
print(i)local i = 0
while i < 10 do
i += 1
end
print(i)In this example Foo is only a type and cannot be instantiated.
declare interface Foo { bar: string }type Foo = {
read bar: string
}In this example Foo cannot be used as a constraint to other interfaces.
sealed interface Foo { bar: string }type Foo = {
read bar: string
}After statements are a shorthand to task.delay. They never yield.
after 100ms {
print("done!");
}task.delay(0.1, function(): ()
print("done!")
end)let collection = [1, 2, 3, 4];
for let n in collection {
print(n);
}const collection = {1, 2, 3, 4}
for n in collection do
print(n)
endfor let n in 1..10 print(n)for n in 1, 10 do
print(n)
endfor let n in 10..1 print(n)for n in 10, 1, -1 do
print(n)
endlet condition = true
let value = condition ? 69 : none;const condition = true
const value = if condition then 69 else nilIn this example K resolves to number | "bar" | "baz".
interface Foo {
[number]: string;
bar: string;
baz: number;
}
type K = keyof(Foo);type Foo = {
read [number]: string,
read bar: string,
read baz: number
}
type K = keyof<Foo>Contributions are welcome! Please read our Contributing Guide for details on the process for submitting pull requests and building language features.
This project is licensed under the Apache-2.0 License - see the LICENSE file for details.