Skip to content

R-unic/loom

Repository files navigation

Loom

CI Status Coverage Status License: Apache-2.0

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.

Features

  • 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 expressions1..10 for slicing and bounds
  • nameof operator – 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

Upcoming Features

  • typeof
  • x 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

Working Examples

let x: bool = false;
const x: boolean = false

mut x = 1;
local x = 1

let 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> = false

Loom 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 = 256

mut x = 69;
x = 420;
local x = 69
x = 420

mut x = 69;
mut y = 420;
let z = x = y = 1;
local x = 69
local y = 420
y = 1
x = y
const z = x

Loom supports shorthand function bodies that return single expressions.

fn one -> 1;
const function one()
    return 1
end

fn id<T>(value: T) -> value;
const function id<T>(value: T)
    return value
end

fn 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] = x

The 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 }).minimum

let 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 = 70

They can also be used with strings.

enum Tag: string {
    Lava = "lava",
    Something = "something"
}
let tag = Tag.Lava
type 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"
end

Declare 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 + 1

let unknown = 69 as unknown;
const unknown = (69 :: unknown)

type Callback = fn(): void
type 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 = 69

mut 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)
end

for let n in 1..10 print(n)
for n in 1, 10 do
    print(n)
end

for let n in 10..1 print(n)
for n in 10, 1, -1 do
    print(n)
end

let condition = true
let value = condition ? 69 : none;
const condition = true
const value = if condition then 69 else nil

In 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>

Contributing

Contributions are welcome! Please read our Contributing Guide for details on the process for submitting pull requests and building language features.


License

This project is licensed under the Apache-2.0 License - see the LICENSE file for details.

Releases

No releases published

Contributors

Languages