Samples
Patterns to learn VexaScript
Focused VexaScript snippets covering operator overloads, JSX control blocks, 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(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}`
}
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
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(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}`)
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")
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 func 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.
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)
}
for (n of 1 ... 5) {
console.log(n)
}
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()
} else {
animal.bark()
}
}
func greetWithInstanceof(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow()
}
}
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
}
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())
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.
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))
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
}
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)
@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)
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)
}