Language Syntax

Syntax

Options

Options go at the top of a source file and are used to configure the output of Blink.

option [OPTION] = [VALUE]

Casing

Default: Pascal
Options: Pascal, Camel, Snake
Controls the casing with which event/function methods generate.

option Casing = Camel

ServerOutput, ClientOutput, TypesOutput

These options allow you to specify where Blink will generate files.

option TypesOutput = "../Network/Types.luau"
option ServerOutput = "../Network/Server.luau"
option ClientOutput = "../Network/Client.luau"

Typescript

Default: false
Tells Blink whether to generate TypeScript definition files alongside Luau files.
The generated d.ts files are placed in the same path as your output files.

option Typescript = true

UsePolling

Default: false Instructs the compiler to automatically output all events with a polling API.

option UsePolling = true

FutureLibrary and PromiseLibrary

In order to use future and promise yield types with functions a path to each library used must be specified

option FutureLibrary = "ReplicatedStorage.Packages.Future"
option PromiseLibrary = "ReplicatedStorage.Packages.Promise"

WriteValidations

Default: false Controls if Blink will check types when writing them (firing an event/invoking a function). Helpful for debugging and during development, but it might result in degraded performance. It is encouraged you disable this option in production.

💡

Blink only checks for builtin primitives. For example if a number was passed. More complicated types like structs, maps and enums cannot be validated.

ManualReplication

Default: false
Controls if Blink will replicate events and functions automatically at the end of every frame.
When set to true automatic replication will be disabled and a StepReplication function will be exposed instead.

Primitives

You can define a primitive using the type keyword
Blink supports the following primitives:

NameSize (Bytes)Supports rangesMinimumMaximumComponents
u81 ByteYes0255No
u162 BytesYes065,535No
u324 BytesYes04,294,967,295No
i81 ByteYes-128127No
i162 BytesYes-32,76832,767No
i324 BytesYes-2,147,483,6482,147,483,647No
f162 BytesYes-6550465504No
f324 BytesYes−1677721616777216No
f648 BytesYes-2^532^53No
vector12 BytesYes (Magnitude)N/AN/AYes (1)
bufferN/AYes (buffer.len)N/A65,535 BytesNo
stringN/AYes (string.len)N/A65,535 BytesNo
boolean1 ByteNoN/AN/ANo
CFrame24 BytesNoN/AN/AYes (2)
Color312 BytesNoN/AN/ANo
Instance4 BytesNoN/AN/ANo
unknownN/ANoN/AN/ANo

Attributes

A type can be marked optional by appending ? at the end.

Arrays can be defined by appending [SIZE] or [[MIN]..[MAX]] after the type declaration.

Primitives can be constrained to ranges by writing ([MIN]..[MAX]) after the primitive type. Ranges are inclusive.

Components can be specified using the angled brackets <>, they allow you to specify what numerical type (u8, u16, u32, i8, i16, i32, f16, f32, f64) to use for vector and CFrame axes.
For example vector<i16> will define a vector that represents its axes using i16.
CFrames take two components CFrame<f32, f32>, one representing position and one representing rotation in order.

💡

Using non float types for CFrame will result in the rotation being reset to 0.

type Simple = u8
type Optional = u8?
type Array = u8[1]
type Range = u8[0..100]
type VectorInt16 = vector<int16>
type Orientation = CFrame<int16, f16>
map Players = {[u8]: Instance(Player)}[0..255]
enum States = (A, B, C, D)[0..255]
struct Dictionary {
    Field: u8
}[0..255]

Sets

You can define a set using the set keyword.
Sets are a string and boolean key value pair dictionary, they can be used to send a set of flags over the network more efficiently than can be done with other data types like structs.

Sets with up to 32 flags are bit-packed to reduce bandwidth usage.
Going over the limit will cause flags to be encoded using 1 byte per flag.
It is recommended to split sets containing over 32 flags into multiple sets instead of using one.

set GameFlags = {
    EntitiesSpawn,
    PlayersCanOpenChests,
    PlayersCanDamagePlayers,
}
 
event SetGameFlags {
    From: Server,
    Type: Reliable,
    Call: SingleSync,
    Data: GameFlags
 
}
local GameFlags: Blink.GameFlags = {
    EntitiesSpawn = true,
    PlayersCanOpenChests = true,
    PlayersCanDamagePlayers = false
}
 
Blink.SetGameFlags.FireAll(GameFlags)

Enums

You can define enums using the enum keyword.
Blink has two type of enums, unit and tagged enums.

Unit Enums

Unit enums represent a set of possible values.
For example, a unit enum representing the state of a car engine:

enum State = { Starting, Started, Stopping, Stopped }

Tagged Enums

Tagged enums represent a set of possible variants with some data attached to each.
They are defined using a string which represents the tag field name.
Each variant is defined by a tag, followed by a struct.

struct Vector2 {
    X: u16,
    Y: u16
}
 
enum Buttons = {Left, Right, Middle}
enum MouseEvent = "Type" {
    Move {
        Delta: Vector2,
        Position: Vector2,
    },
    Drag {
        Delta: Vector2,
        Position: Vector2
    },
    Click {
        Button: Buttons,
        Position: Vector2
    },
}

Structs

You can define structs using the struct keyword
Structs can also hold structs within:

struct Entity {
    Identifier: u8,
    Health: u8(0..100),
    State: ( Dead, Alive )?,
    Direction: vector(0..1),
    Substruct: struct {
        Empty: u8[0],
        Complex: u8[1..12],
        Vector: UnitVector,
    }?
}

Generics


Structs, tagged enums and maps support the use of generic type parameters, a generic is simply a type which allows you to slot in any other type, generics can be very handy in reducing repetition.

struct Packet<T> {
    Sequence: u16,
    Ack: u16,
    Data: T
}
 
struct Entity {
    Identifier: u8,
    Health: u8(0..100),
    Angle: u16,
    Position: vector
}
 
struct Command {
    X: u8,
    Y: u8,
    Z: u8,
    -- ...
}
 
event Snapshot {
    From: Server,
    Type: Unreliable,
    Call: SingleSync,
    Data: Packet<Entity[]>
}
 
event Command {
    From: Server,
    Type: Unreliable,
    Call: SingleSync,
    Data: Packet<Command>
}

In the code above we have a simple packet transmission protocol which contains the current packets identifier (Sequence), the last recieved packet (Ack) and a generic data field. Instead of repeating the contents of Packet everytime we need to send something over the wire we can take advantage of generics to automatically fill the Data field with whatever we need to transmit.

Merging

Structs can merge other structs into themselves, this is equivalent to a table union in Luau. When merging avoid duplicate fields as they will result in a compilation error.

struct a {
    foo: u8
}
 
struct b {
    bar: u8,
}
 
struct c<A, B, C> {
    a: A,
    b: b,
    c: C
} 
 
struct merge {
    ..a,
    ..b,
    ..c<u8, u8, u8>,
}

Produces the following Luau type for merge:

type merge = {
    foo: number,
    bar: number,
    a: number,
    b: number,
    c: number
}

Maps

You can define maps using the map keyword

Maps cannot have optional keys or values as there is no way to represent those in Luau.

map Example = {[string]: u8}

Instances

Instances are another type of Primitive and as such they can be defined using the type keyword

type Example = Instance
type Example = Instance(ClassName) -- You can also specify instance class
💡

If a non optional instance results in nil on the recieving side it will result in an error, this may be caused by various things like streaming, players leaving etc.
In order to get around this you must mark instances as optional.

Tuples

Tuples can be defined using brackets ().
Tuples can only be defined within the data field of an event/function.

event Example {
    From: Server,
    Type: Reliable,
    Call: SingleSync,
    Data: (u8, u16?, Instance, Instance?, u8[8])
}
 

Events

You can define events using the event keyword
Events have 4 fields:
From - Client or Server
Type - Reliable or Unreliable
Poll - true or false - Controls whether the event will generate using the polling API Call - SingleSync, SingleAsync, ManySync, ManyAsync - Many refers to multiple connections Data - Can hold either a type definition or a reference to an already defined type, omit for events with no data.

event Simple {
    From: Client,
    Type: Unreliable,
    Call: SingleSync,
    Data: u8
}
 
event Reference {
    From: Client,
    Type: Unreliable,
    Call: SingleSync,
    Data: Entity
}
 
event Complex {
    From: Client,
    Type: Unreliable,
    Call: SingleSync,
    Data: struct {
        Field: u8
    }
}

Polling

When Poll is true or the UsePolling option is true, events will generate with a polling API on the listener side, the On function is replaced with a Iter function which returns an iterator to be used within a for loop.

event ReplicateEntities {
    From: Server,
    Type: Unreliable,
    Call: SingleSync,
    Poll: true,
    Data: map {[u8]: unknown[]}
}
for Index, EntityMap in Blink.ReplicateEntities.Iter() do
    ...
end

Functions

You can define functions using the function keyword
Functions have 3 fields:
Yield - Coroutine or Future or Promise
Deifnes what library will be used to handle invocations
Data - Can hold either a type definition or a reference to an already defined type, omit for functions with no data. Return - Can hold either a type definition or a reference to an already defined type, omit for functions with no return.

function Example {
    Yield: Coroutine,
    Data: u8,
    Return: u8
}
 
function ExampleFuture {
    Yield: Future,
    Data: u8,
    Return: u8
}
 
function ExamplePromise {
    Yield: Promise,
    Data: u8,
    Return: u8
}

Scopes (Namespaces)

You can define a scope (namespace) using the scope keyword
Scopes allow you to group similiar types together for further organization

Defining scopes:

scope ExampleScope {
    type InScopeType = u8
    event InScopeEvent {
        From: Server,
        Type: Reliable,
        Call: SingleSync,
        Data: u8
    }
}
 
struct Example {
    Reference = ExampleScope.InScopeType
}

Scopes automatically inherit any declarations made within their parent scopes (including main)

type Example = u8
scope ExampleScope {
    type TypeInScope = u8
    scope ExampleNestedScope {
        -- Example is taken from the main (global) scope
        -- TypeInScope is taken from the parent scope (ExampleScope)
        map ExampleMap = {[Example]: TypeInScope}
    }
}

Using scopes in code:

local Blink = require(PATH_TO_BLINK)
Blink.ExampleScope.InScopeEvent.FireAll(0)
 
local Number: Blink.ExampleScope_InScopeEvent = 0

Imports

💡

Imports are not currently supported when compiling using the Studio Plugin.

Blink allows you to use multiple definition files through importing.
Imports will pull all declarations (events, functions, scopes and types) from the target file and load them into the importing scope as a new scope using either the file name or a user provided name through the as keyword.

import "../path/File.blink" --> Imports as a new scope "File"
type a = File.b
import "../path/File.blink" as Something --> Imports as a new scope "Something"
type a = Something.b
scope MyScope {
    --> Imports a new scope "Something" into "MyScope"
    import "../path/File.blink" as Something
}
type a = MyScope.Something.b

Limitations

Referencing types

A type must be defined earlier in the source for it to be referenceable, you cannot reference a type before it has been defined.

struct Invalid {
    Field: Number
}
type Number = u8
[ERROR]
    |
002 |     Field: Number
    |             ^^^^^^ Unknown type referenced.