Samples

Patterns to learn VexaScript

Focused VexaScript snippets covering operator overloads, JSX control blocks, 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(const x: number, const 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(":")
  }
}

const grid = Grid<string>()
grid[2, 4] = "selected"
const cell = grid[2, 4]
const 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"

func Counter({ initial: number }) {
  var count by useState(initial)

  return <button onClick={ { count++ } }>
    Count: {count}
  </button>
}

JSX control blocks

A lightweight eager component factory can combine prop spreading with nested {#for}, {#if}, {:else if}, and {:else} children.

class React {
  static createElement(name: any?, args: any, ...children: any[]) {
    if (typeof name == "function") {
      return name!({ ...args, children })
    } else {
      return { name, args, children }
    }
  }
}
func MyComponent({ items: number[], children: any[] }) {
  return <>
    {children}
    <ul>
    {#for it of items}
      {#if it % 2 == 0}
        <li>{it}?</li>
      {:else if it % 3 == 1}
        <li>{it}.</li>
      {:else}
        <li>{it}!</li>
      {/if}
    {/for}
    </ul>
  </>
}
console.log(<MyComponent items={[1,2,3,4,5,6,7,8,9]}>hello</MyComponent>)

Implicit property access

When no ambiguity happens, this is optional.

class Counter(var value: int) {
  func 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(const width: number, const height: number) : Shape {
  area => width * height
  fill(color: string) => `${color}:${width}x${height}`
}

class ShapeLogger(const shape: Shape, const 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.

func 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.

func 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(const property: Property<number>, const src: number, const dst: number)

func Property<number>.operator[](src: number, dst: number): TweenTarget {
  return TweenTarget(this, src, dst)
}

func tween(target: TweenTarget) {
  target.property.value = target.dst
}

const slider = Slider(5)
const 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 func fetchPrice(item: string): number {
  return fetch(`/prices/${item}`).json()
}

sync func 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 func 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.

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

Array comprehensions

Put a for-of or for-in header inside brackets to collect a typed result array without a temporary accumulator.

const doubled = [for (value of [1, 2, 3]) value * 2]
const normal = [for (n in 0 ..< 10) n]
const labels = [for (const [name, score] of entries) "$name:$score"]
const conditionalMixed = [
  for (n in 0 ... 9) if (n % 2 == 0) n else n * 3,
  for (n in 0 ... 9) if (n % 2 == 0) n,
]
const mixed = [1, for (n in 0 ... 9) n, ...items, for (n in 0 ... 9) n * 2, 0]

Cascade operator

.. keeps applying member operations to the same receiver and then returns that receiver, which is handy for configuration-style code.

const 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.

const 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() {} }

func greet(animal: Cat | Dog) {
  if (animal is Cat) {
    animal.meow()   // type narrowed to Cat here
  } else {
    animal.bark()   // type narrowed to Dog here
  }
}

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

func 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.

func 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.

const 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.

const doubled = [1, 2, 3].map { it * 2 }

const even = [1, 2, 3, 4].filter { it % 2 == 0 }

const 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(const milliseconds: number)

const number.milliseconds => Duration(this)
const number.seconds: Duration => Duration(this * 1000)
const number.minutes: Duration => Duration(this * 60_000)

const d1 = 500.milliseconds
const d2 = 2.seconds
const d3 = 1.minutes

Receiver functions

Generic receiver functions can accept a block that operates on the receiver and returns it for fluent calls.

func <T> T.apply(block: T.() => T) { block(this); return this }

class Point(var x: number, var y: number)

const 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>.

func <T> Array<T>.second(): T => this[1]
const <T> Array<T>.doubledLength => length * 2

const 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.

func connect(host: string, port: number, tls: boolean = false) {}

connect(port: 8080, host: "localhost")
connect("localhost", port: 8080, tls: true)

class Point(const x: number, const y: number)
const 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
}

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

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

const rect = Rect(x: 10, y: 20, width: 320, height: 180)

// Output fields populated by native code use var!.
@FFIStruct(56)
@FFIAlign(8)
class SDLEvent {
  @FFIOffset(0) @FFISize(4) var! type: int
  @FFIOffset(12) @FFISize(1) var! keyState: int
}

@FFILibrary("SDL2.dll", "libSDL2.so", "SDL2.framework/SDL2")
declare class SDL2 {
  @FFIName("SDL_PollEvent") static PollEvent(event: SDLEvent): int
}

const event = SDLEvent()
SDL2.PollEvent(event) // native code initializes the var! fields

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
}

const pointer = NativeMemory.malloc(8L)
pointer.setInt32(0, 1234)

const 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 func nextFrame(): void {
  SDL2Async.Delay(16)
}