Samples

Patterns to learn VexaScript

Focused VexaScript snippets covering operator overloads, indexers, property references, delegates, sync functions, ranges, extensions, and more.

Classes & Objects

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}`
}

Delegated Properties

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   // function delegate: reads source
var total by box(0)            // object delegate: reads/writes .value

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}`)

Async & Sync

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")   // auto-awaited
  const tax = fetchPrice("tax")     // auto-awaited
  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 {
  // fire-and-forget — result Promise kept, not awaited
  const pending: Promise<number> = go fetchPrice("audit")

  // normal sync call — awaited automatically
  const price = fetchPrice("book")

  console.log(price)
  console.log(await pending)
}

Control Flow

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)    // 0, 1, 2, 3, 4
}

for (n of 1 ... 5) {
  console.log(n)    // 1, 2, 3, 4, 5
}

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()   // type narrowed to Cat here
  } else {
    animal.bark()   // type narrowed to Dog here
  }
}

fun greetWithInstanceof(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    animal.meow()  // same smart cast as `is`
  }
}

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   // implicit return
}

Extensions & Calls

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())        // 20
console.log(xs.doubledLength)   // 6

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))       // "int:42"
console.log(describe("hello"))  // "str:hello"

Native & Interop

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)
}