Samples
Patterns to learn VexaScript
Focused VexaScript snippets covering operator overloads, indexers, property references, delegates, sync functions, ranges, extensions, and more.
Operator overloading
Operator and method overloading and new-less constructions, for concise writing.
class Vec2(val x: number, val y: number) {
operator+(other: Vec2) => Vec2(x + other.x, y + other.y)
operator-(other: Vec2) => Vec2(x - other.x, y - other.y)
}
Vec2(1, 2) + Vec2(3, 4)
Index operator overloads
Classes can overload [] and []= with one or more index arguments, including rest dimensions.
class Grid<T> {
var data: T[] = []
operator[](x: int, y: int): T {
return data[y * 10 + x]
}
operator[]=(value: T, x: int, y: int) {
data[y * 10 + x] = value
}
}
class PathKey {
operator[](...dimensions: int[]): string {
return dimensions.join(":")
}
}
val grid = Grid<string>()
grid[2, 4] = "selected"
val cell = grid[2, 4]
val key = PathKey()[2, 4, 8]
JSX with Preact
Typed prop destructuring stays concise, while a delegated useState tuple turns mutable state into direct reads and assignments.
import { h } from "preact"
import { useState } from "preact/hooks"
fun Counter({ initial: number }) {
var count by useState(initial)
return <button onClick={ { count++ } }>
Count: {count}
</button>
}
Implicit property access
When no ambiguity happens, this is optional.
class Counter(var value: int) {
fun increment(): int => ++value
}
Class delegates
Satisfy an interface by forwarding its members to another value using by.
interface Shape {
area: number
fill(color: string): string
}
class Rectangle(val width: number, val height: number) : Shape {
area => width * height
fill(color: string) => `${color}:${width}x${height}`
}
class ShapeLogger(val shape: Shape, val label: string) : Shape by { shape } {
describe() => `${label}: area=${area}`
}
Tuple delegate
A [value, setter] tuple delegate wires reads and writes through custom accessors — like React's useState.
fun useState(value: number) {
return [() => value, (newValue: number) => { value = newValue }]
}
var count by useState(0)
count = count + 1
count += 1
count++
Object & function delegates
A { value } object or a zero-argument function also work as delegates — all assignments route through the accessor.
fun box<T>(initial: T) {
return { value: initial }
}
var source = 1
var observed by () => source
var total by box(0)
total = observed + 2
source = 5
total += observed
total++
Property references
expr::field creates a live Property<T> reference with name and value, so it works with delegates and APIs that animate or bind properties.
class Slider(var x: number)
class TweenTarget(val property: Property<number>, val src: number, val dst: number)
fun Property<number>.operator[](src: number, dst: number): TweenTarget {
return TweenTarget(this, src, dst)
}
fun tween(target: TweenTarget) {
target.property.value = target.dst
}
val slider = Slider(5)
val xRef = slider::x
var x by xRef
x += 10
tween(slider::x[0, 100])
console.log(`${xRef.name}:${xRef.value}`)
Sync functions
sync functions auto-await any Promise-typed expression. Write sequential code without explicit await.
sync fun fetchPrice(item: string): number {
return fetch(`/prices/${item}`).json()
}
sync fun checkout(): number {
const base = fetchPrice("book")
const tax = fetchPrice("tax")
return base + tax
}
The go operator
Inside a sync function, prefix an expression with go to keep the raw Promise instead of auto-awaiting it.
sync fun main(): void {
const pending: Promise<number> = go fetchPrice("audit")
const price = fetchPrice("book")
console.log(price)
console.log(await pending)
}
Defer
defer schedules cleanup for the end of the block — it runs even when the block returns early or throws.
fun readValue(): int {
console.log("open")
defer console.log("close-2")
defer console.log("close-1")
console.log("read")
return 7
}
Range expressions
... is end-inclusive; ..< is end-exclusive. Both work directly in for-of loops and as values.
for (n of 0 ..< 5) {
console.log(n)
}
for (n of 1 ... 5) {
console.log(n)
}
Cascade operator
.. keeps applying member operations to the same receiver and then returns that receiver, which is handy for configuration-style code.
val badge = new Graphics()
..point = Vec2(centerX, centerY - 16)
..beginFill(0xff6b35)
..drawRoundedRect(-110, -64, 220, 128, 28)
..endFill()
Postfix receiver blocks
value. { ... } evaluates a value once, makes it the implicit receiver inside the block, and returns that same value for grouped configuration and mutation.
val badge = new Graphics(). {
point = Vec2(centerX, centerY - 16)
beginFill(0xff6b35)
drawRoundedRect(-110, -64, 220, 128, 28)
endFill()
}
Smart casts
is keeps nominal instanceof behavior for classes and also accepts primitive, literal, object, array, regular-expression, relational, and, and or patterns.
class Cat { meow() {} }
class Dog { bark() {} }
fun greet(animal: Cat | Dog) {
if (animal is Cat) {
animal.meow()
} else {
animal.bark()
}
}
fun greetWithInstanceof(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow()
}
}
fun clamp(value: int | string) {
if (value in 0 ... 100) {
const safe: int = value
}
}
Subject match and bindings
A subject is evaluated once. val name captures a matched value; val name: Type checks and captures it with a branch-local type.
fun describe(packet: any): string {
return match (packet) {
{ kind: "ok", payload: [val first: string, ...] } ->
"first=" + first
[string, val count: number, 3] ->
"count=" + count
else -> "unknown"
}
}
Composable matcher patterns
Use literals, primitive types, regular expressions, open array shapes, relational checks, and and/or. The compact arrow form omits when; the colon form requires it.
val label = match (value) {
/^user-[0-9]+$/i -> "user id"
>= 10 and < 20 -> "teen"
"ready" or "running" -> "active"
string -> "other text"
else -> "unknown"
}
if (value is ({ kind: "ok" } and { payload })) {
console.log(value.payload)
}
Tail lambdas
A lambda after the closing parenthesis — or as the only argument — follows Kotlin/Swift style and reduces visual noise.
val doubled = [1, 2, 3].map { it * 2 }
val even = [1, 2, 3, 4].filter { it % 2 == 0 }
val result = [1, 2, 3].map {
const tripled = it * 3
tripled + 1
}
Extension properties
Add read-only properties to existing types. Import them where needed; access without an import is an error.
class Duration(val milliseconds: number)
val number.milliseconds => Duration(this)
val number.seconds: Duration => Duration(this * 1000)
val number.minutes: Duration => Duration(this * 60_000)
val d1 = 500.milliseconds
val d2 = 2.seconds
val d3 = 1.minutes
Receiver functions
Generic receiver functions can accept a block that operates on the receiver and returns it for fluent calls.
fun <T> T.apply(block: T.() => T) { block(this); return this }
class Point(var x: number, var y: number)
val point = Point(10, 20).apply {
x = y * 2
this
}
Generic extension methods
Extension methods can be generic and work with built-in collection types like Array<T>.
fun <T> Array<T>.second(): T => this[1]
val <T> Array<T>.doubledLength => length * 2
val xs = [10, 20, 30]
console.log(xs.second())
console.log(xs.doubledLength)
Named arguments
Pass arguments by parameter name in any order. The compiler reorders them to match the callee's parameter list.
fun connect(host: string, port: number, tls: boolean = false) {}
connect(port: 8080, host: "localhost")
connect("localhost", port: 8080, tls: true)
class Point(val x: number, val y: number)
val p = Point(y: 2, x: 1)
Function overloads
Multiple functions can share the same name when their parameter types differ. The compiler picks the right one at each call site.
function describe(value: int): string { return "int:" + value }
function describe(value: string): string { return "str:" + value }
console.log(describe(42))
console.log(describe("hello"))
Cross-backend FFI
Declare a C ABI once and try platform library candidates in order. The same typed call uses Deno FFI or the native C++ runtime.
@FFILibrary("libSystem.B.dylib", "libc.so.6", "msvcrt.dll")
declare class NativeC {
static abs(value: int): int
}
val distance = NativeC.abs(-42)
Renamed FFI symbols
@FFIName maps a clean VexaScript method name to the exported C symbol without changing call sites.
@FFILibrary("SDL2.dll", "libSDL2.so", "SDL2.framework/SDL2")
declare class SDL2 {
@FFIName("SDL_Init")
static Init(flags: int): int
}
val status = SDL2.Init(32)
FFI struct layouts
@FFIStruct creates an ArrayBuffer-backed ABI layout. Alignment, offsets, and field sizes remain explicit and portable.
@FFIStruct(16)
@FFIAlign(4)
class Rect(
@FFIOffset(0) @FFISize(4) var x: int = 0,
@FFIOffset(4) @FFISize(4) var y: int = 0,
@FFIOffset(8) @FFISize(4) var width: int = 0,
@FFIOffset(12) @FFISize(4) var height: int = 0
)
val rect = Rect(x: 10, y: 20, width: 320, height: 180)
FFI pointers & buffers
FFIPointer exposes typed memory access, while ArrayBuffer arguments pass their backing bytes without a copy.
@FFILibrary("libSystem.B.dylib", "libc.so.6", "msvcrt.dll")
declare class NativeMemory {
static malloc(size: long): FFIPointer
static memset(bytes: ArrayBuffer, value: int, size: long): FFIPointer
static free(pointer: FFIPointer): void
}
val pointer = NativeMemory.malloc(8L)
pointer.setInt32(0, 1234)
val bytes = ArrayBuffer(4)
NativeMemory.memset(bytes, 65, 4L)
NativeMemory.free(pointer)
Nonblocking FFI calls
A foreign method returning Promise<T> runs without blocking the main event loop and works naturally inside a sync function.
@FFILibrary("SDL2.dll", "libSDL2.so", "SDL2.framework/SDL2")
declare class SDL2Async {
@FFIName("SDL_Delay")
static Delay(milliseconds: int): Promise<void>
}
sync fun nextFrame(): void {
SDL2Async.Delay(16)
}