Language Features
Complete reference for Loxel v2 — a modern, expression-oriented language with optional static typing, multilingual keywords, and built-in concurrency.
The statuses listed on the page are for developers only, and do not reflect the delivered binary. Everything is currently in beta, and subject to change.
Variables & Constants ✅ Working
Use let for mutable variables and const for immutable constants. Type annotations are optional; Loxel infers types automatically.
Example
Keywords: Strict vs Contextual ✅ Working
Loxel splits its keywords into two classes (the C#/TypeScript/Kotlin tradition). Hard (strict) keywords always mean their grammar construct and can never name a variable, parameter, function, or class — binding one is an immediate parse error. Contextual keywords only act as keywords in their exact grammar position; everywhere else they are ordinary identifiers.
Hard (24): if else for while break continue return throw try catch match await typeof def class let const import export true false nil self super
Contextual (21): as from in is extends implements uses case default static readonly public private protected async unsafe namespace type struct interface trait
Two extra rules: member position is always free — after ., in field declarations, and as map keys, every keyword (hard ones included) is an ordinary name. And all translations of a keyword behave identically: Spanish si is exactly as reserved as if, while tipo is exactly as bindable as type — and builtins like .namespace respond to every language's spelling. print is no longer a keyword at all — it is a builtin function (print(x), any language spelling), so let print = 42; binds and reads normally.
Example
Expressions & Blocks ✅ Working
Everything in Loxel is an expression that returns a value — if, match, try, and blocks all yield results.
Example
Only a bare trailing expression becomes the block's value — a trailing def is a declaration, not an expression, so it is not picked up:
Strings & HEREDOC ✅ Working
Backtick strings and HEREDOC both interpolate with ${}. HEREDOC (<<<END) just lets that span multiple lines. The nowdoc form (<<<'END') treats ${} as literal text instead of interpolating it. In a backtick string, a backslash escapes the ` and the ${ — see below.
String interpolation
Escaping ` and ${
A run of backslashes immediately in front of a $ follows parity: pairs collapse, and an odd leftover escapes the ${. That position is the only one where backslashes behave differently, because it is the only one where they are about interpolation.
HEREDOC multi-line strings
Character-aware indexing & byteLength (v3.5.0)
Control Flow ✅ Working
Standard if/else, while, C-style for, range-based for-in, and comprehensions. (do-while is 📋 planned.)
Example
Type Annotations ✅ Working
Types are optional but recommended for public APIs. Loxel supports three type-checking modes: OFF, WARN, and STRICT.
Primitive types & inference
Function (callable) types
A function type (A, B) => R can be written inline on a parameter, aliased with type, applied to a binding, and nested inside other types (Map<String, MathOp>). In the native compiler the signature is enforced at compile time — both when a function is assigned to a function-typed binding and when a value is called through such a type. Enforcement is best-effort and conservative: arity is always checked; parameter types are checked when the lambda annotates them (an unannotated (a, b) => … infers Any params — Loxel has no contextual typing); the return type is checked when annotated or inferable; lambdas with default or rest parameters stay permissive. The VM/tree-walker do not run the static checker, matching every other typed-binding contract.
Exact types & checked narrowing
Generics ✅ Working
Generic classes and functions use <T> type parameters. Type bounds use extends. Since v3.4.0 a superclass reference can carry type arguments (class Orders < Map<String, Number>) — closing, forwarding, or partially closing the parent's parameters — with wrong arity or bound violations reported as clear errors. Works in the tree-walker, bytecode VM, and native/LLVM backend. Box<Number> is a first-class parameterized-class value in all three modes — it can be aliased, passed around, and called later (including with named constructor arguments). Method-level type arguments (obj.method<Number>(x)) are validated in all three modes: type-argument arity, declared bounds, and parameter types. Type arguments are erased for execution (as in most dynamically-typed languages) but recorded on instances for runtime checking of generic method parameters; the native backend additionally validates instantiation arity, non-generic misuse, and type bounds at compile time when the class is statically known, and at runtime otherwise.
Example
Union & Optional Types ✅ Working
Union types express "this OR that" with |. T? is shorthand for T | Nil.
Example
Classes ✅ Working
Classes use def init for constructors and self (not this) for the instance reference. The @field syntax is not supported in v2.
Example
Inheritance ✅ Working
Single inheritance with <, or its alias extends (accepted in class headers since #907; it also remains the generic type bound, <T extends Number>). Use super.init() to call the parent constructor and super.method() to call overridden methods.
Example
Interfaces & Traits ✅ Working
Interfaces define contracts. Traits (mixins) provide reusable implementation. A class implements interfaces and uses traits.
Interface
Traits (mixins)
Property Promotion ✅ Working
Annotate constructor parameters with visibility/mutability modifiers to auto-create fields. Supported in the tree-walker, bytecode VM, and native/LLVM backend.
Example
Visibility Modifiers ✅ Working
public (default), private, and protected control access to fields and methods. Enforced in all three modes, including the native/LLVM backend. Native (and the tree-walker) raise catchable errors; the bytecode VM raises the same messages but treats them as fatal. Note on inheritance: native resolves the calling class lexically, so a base-class method (e.g. via super.init) can always touch the base's own private/protected fields, and subclasses can access inherited protected (but not private) fields — the tree-walker and VM currently derive the calling class dynamically and each rejects one of those base-method patterns.
Example
Operator Overloading ✅ Working
Define a method whose name is the operator symbol — def +(other) — to customize behavior for +, -, *, **, /, %, ==, !=, <, >, <=, >=, <=>, the bitwise |, &, ^, <<, >>, and the unary -@ / !.
Example
Compound-assignment contract: in-place += vs. allocating +
A class may also define +=, -=, *=, /=, or %= directly, distinct from its +-style method — mirroring Python's __iadd__ vs. __add__. x += y tries the class's own += method first (mutate self in place, no allocation) and falls back to + when += isn't defined.
Decorators ⚠️ Partial
Decorators wrap functions, classes, or methods. The native compiler implements the full surface: user function decorators (stacking, decorated recursion — the @memoize fibonacci pattern — and use as values), class decorators, user-defined method decorators, and the built-in method decorators @cached/@deprecated/@logged/@timed; still-unsupported decorators (@computed, @onInherit) emit a compile-time warning instead of silently dropping. Method-decorator convention: the decorator receives the unbound method — the receiver is its explicit first parameter (any name; self is a keyword) — and returns a callable invoked as (receiver, args...); decorations inherit to subclasses and compose under the built-ins (@cached memoizes the wrapped call). Earlier interpreted-mode gaps are resolved: the VM applies user function decorators (stacking, factory form) and user method decorators (#399 fixed), and the tree-walker applies function and class decorators; @cached hits no longer corrupt the VM stack and @deprecated("reason") reasons are preserved. The remaining gap is the tree-walker's user method decorators: it calls the wrapper without the receiver, so the unbound convention breaks there (#947; built-ins work in all three modes).
Example
Match Expressions ✅ Working
Nine pattern types: literal, variable, wildcard, array, object/map, type, range, guard, and OR. Works in all modes, including the array rest sub-pattern ([head, ...tail], promoted to the VM and native by #946) — the rest binds the remaining elements as a new array, and guards can reference it.
Core patterns
Match as expression with complex example
Operators ✅ Working
Arithmetic (incl. exponentiation **), bitwise (| & ^ << >>), comparison (incl. strict === / !==), membership (~> / <~, Unicode ∈ ∋ ∉ ∌), logical, assignment, null-handling, pipeline, spread, and type-checking operators.
The ? family, one rule: thirteen constructs spell “this operand might not be what you want” — access (?. ?.[] ?.() T?), choice (? : ?: ??), conditional assignment (??= ?= =?? =?), and railway (?> ?!). In every one, the ? sits next to the operand being questioned, so the symbol itself tells you what gets tested. The assignment and railway forms are detailed below; access and Result behavior have their own sections.
All operators quick reference
Operator precedence (highest → lowest)
Functions & Parameters ✅ Working
Named functions with def, arrow shorthand, default parameters, named arguments (colon syntax only), rest parameters (PHP-style unified map), lambdas, and higher-order functions.
Function syntax overview
Lambdas, closures & higher-order functions
Async / Await ✅ Working
Async functions return Promise<T>. await suspends execution until the Promise resolves. Use Promise.all for concurrent requests. Known tree-walker bug: a top-level await fn() currently runs the async body twice there (#948); the VM and native run it once.
Example
Channels & Spawn 🚧 In Progress
Channel-based concurrency and thread spawning. Channel and Thread.spawn/Thread.join work in the bytecode VM and native mode (the tree-walker cannot call them — #747); for await over a channel and a global spawn() are 📋 planned syntax, not yet parsed.
Example
Enums 🚧 In Progress
Enums define a fixed set of named variants. Variants can carry associated data (like Rust enums) and work with pattern matching.
Example
Structs 📋 Planned
Structs are designed as value types — copied on assignment — for small, immutable data (Point, Color, Vec2). Design only today: the parser accepts comma-separated field declarations, but methods in a struct body and struct construction are not implemented in any backend yet (see issue #912). Use classes in the meantime.
Value semantics vs. class reference semantics
Destructuring ✅ Working
Extract values from arrays, maps, and structs using destructuring assignment and function parameter patterns.
Array & object destructuring
Arrays ✅ Working
Dynamic arrays with rich built-in methods. Slice with arr[1:3] (exclusive end), stride with arr[::2], reverse with arr[::-1], and select or filter numpy-style with fancy indexing (arr[[0, 2]] or a boolean mask). Since v3.4.0, sort/sortBy/reverse return a new array and leave the receiver untouched, and the comparator sort is a stable O(n log n) merge sort in every backend.
Array operations
Maps ✅ Working
Key-value dictionaries with property shorthand, spread, and comprehensive methods including entries(), clear(), and empty(). Data keys always win over method names on dot access, so v3.4.0 added non-shadowable static forms (Map.keys(m), Map.fromEntries(pairs), …) for code that must survive arbitrary JSON — and Self in a static resolves to the class it was invoked through, so subclasses inherit constructing statics that build the subclass.
Map operations
Sets & Tuples ✅ Working
Sets store unique values with union/intersection/difference operations. Tuples (v3.4.0) are fixed-arity, immutable, per-position-typed sequences — a distinct core class with the array read surface (indexing, length, iteration, destructuring) and every mutator rejected.
Sets
Tuples
Module System ✅ Working
Per-module scope isolation, export-only visibility, and singleton execution. Supports stdlib (std::), relative paths (./), and bare package names.
Defining & importing a module
Import forms & module singletons
Function-body imports & breaking module cycles
import is an ordinary statement — inside a function body it runs at call time, after all modules have loaded. Since loaded modules are cached and the circular-dependency check only guards loads in progress, this is the sanctioned way for two modules to refer to each other. The same pair with both imports at top level fails with Circular dependency detected.
Runtime dynamic loading — Module.load_exports / Module.load_source
A module can be resolved at runtime — from a path or from source text already in hand — and its export table returned as a Map. Same loader as static import: namespace requirement, export-only visibility, singleton caching, circular-import detection. Load failures (missing file, parse error, a module body that throws) are ordinary catchable errors.
Example project — a host program that discovers and loads plugins it was not compiled with:
Compile the host natively — dynamic loading works in compiled binaries too (the loaded modules execute in an embedded interpreter; see the notes below):
Notes that matter:
- Execution modes. In the bytecode VM the full surface works — exported classes are constructible from the returned Map. In a compiled binary, runtime-loaded modules run in an embedded bytecode VM: data and functions bridge across (functions arrive as callable closures), but class exports fail with an explicit error — export a factory function returning a Map instead. Native speed applies only to statically-compiled modules.
- Singletons. A
load_sourcename maps to one module for the life of the process, like a file path; re-registering a used name is an explicit error, never a silent replace. - Security. Loaded source runs with the full privileges of the host program — filesystem, network, process spawning. There is no sandbox. Load only source you trust or have vetted through your own gates; never feed it network input.
Multilingual Keywords ✅ Working
Write code in 7 languages. Keywords, type names, and (since v3.4.0) builtin method names are internationalized. There is no --lang flag: every language's spellings are active simultaneously, and languages can be mixed freely in one file — English is the canonical form.
Examples in multiple languages
| Language | Function | Class | String | Number | .sort() | .keys() |
|---|---|---|---|---|---|---|
| English | def | class | String | Number | sort | keys |
| Español | def | clase | Cadena | Numero | ordenar | claves |
| Français | def | classe | Chaîne | Nombre | trier | clés |
| Deutsch | def | klasse | Zeichenkette | Zahl | sortieren | schlüssel |
| Português | def | classe | Cadeia | Numero | ordenar | chaves |
| 日本語 | def | クラス | 文字列 | 数値 | 整列 | キー |
| 中文 | def | 类 | 字符串 | 数字 | 排序 | 键 |
Error Handling ✅ Working
Standard try/catch blocks (there is no finally clause in the language), and try as an expression — the try block's final expression is the result, the catch block's on a caught throw, identical in all three modes. Error is a real core class (#907): Error("msg") constructs an instance carrying .message, it is subclassable (class ValidationError extends Error, or < Error), thrown errors reach catch unchanged in all three modes, and typed catch clauses — catch (e: NetworkError), tried in order, untyped catch-all last — dispatch on the error's class.
Example
Result & Railway Operators ✅ Working
Recoverable failures as values: Ok(v) and Err(e) build a Result<T, E>, and the railway operators thread it through pipelines. ?> is the failure-aware sibling of |> — an Err skips every remaining stage (the stage is never evaluated), while an Ok payload flows into the next function, which must itself return a Result (bind semantics; use .map() for plain transforms). Postfix ?! unwraps an Ok or early-returns the Err from the enclosing function (like Rust's ?), and ?? gains unwrap-or: Ok(v) ?? d yields v, Err(_) ?? d yields d. Identical in all three modes; ?! at module top level is rejected at compile time in VM and native modes.
Example
Built-in Functions ✅ Working
Core built-ins always available without an import.
Output, type conversion & utilities
String & Math ✅ Working
String methods
Math module
File, HTTP & JSON 🚧 In Progress
File I/O
HTTP Client & JSON
Data Structures ✅ Working (V2)
Import from std::collections, std::trees, and std::graphs.
Stack, Heap, Deque
Binary Search Tree, AVL Tree, Trie
NumPy & Machine Learning ✅ Working (V2)
N-dimensional arrays (std::numpy), ML algorithms (std::ml), and statistical computing (std::statistics).
NumPy-style arrays
ML classifiers & regression
AI Training & LLMs ✅ Working (V2 · compiled)
A full pure-Loxel deep-learning stack — no PyTorch. Threaded f32 tensors and
a fused 4-bit kernel (std::tensor), and real-model QLoRA fine-tuning
plus Q4 KV-cache inference (std::llm), validated end-to-end on
SmolLM2-360M. Instruct models are covered by two submodules:
std::llm::qwen3 loads Qwen3-architecture GGUFs (ternary Bonsai Q2_0 and stock
llama.cpp K-quant Q4_K_M), and std::llm::chat assembles conversations
(templates, transcripts, stop tokens) for GGUF instruct models. This stack targets the
LLVM-compiled backend (the reference for these modules) — compile with
-O2 --cpu=native; it is not run through the interpreter.
QLoRA fine-tuning (4-bit frozen base + trainable adapters)
Q4 KV-cache generation (temperature + top-k)
Instruct models — std::llm::qwen3 loaders & std::llm::chat templates
std::llm::chat is pure conversation assembly — it never generates and never touches the filesystem. std::llm::qwen3 loads Qwen3-architecture GGUFs into the same decoder core that generate_cached runs on.
std::tensor — threaded matmul & fused 4-bit kernel
Hardware Acceleration ✅ Working (V2 · compiled)
One port — std::device — and per-platform adapters behind it. Callers select and
interrogate compute (select() / supports() / describe())
without naming a backend; select() never fails — a missing GPU is a
slower route, not an error, and the CPU path is the reference every accelerator is gated against.
Measured on qwen3-4b Q4_K_M: 0.42 → 33–35 tok/s end-to-end (device-resident
decode on an RTX 2080).
docs/runtime/gpu-acceleration.md.Selecting and probing a device (std::device)
Environment variables (the complete acceleration surface)
| Variable | Read by | Meaning |
|---|---|---|
LOXEL_PERF_MODE=gpu | runtime (one gate) | THE opt-in: permits Vulkan zero-copy buffers, the K-quant CUDA session, and device-resident decode. Unset = pure CPU, always. |
LOXEL_DEVICE | std::device.select() | Selection policy when no explicit arg: cpu, vulkan, cuda, prefer-gpu. |
LOXEL_CHAT_SESSION_CAP | loxel-chat infer worker | Session length in tokens. Unset + CUDA: auto-sized to free VRAM (up to the model's native context). Unset + CPU: 1024. |
LOXEL_CHAT_INFER_HOST | loxel-chat web tier | Which worker serves chat: infer (CPU, default) or infer-gpu (compose --profile gpu). |
LOXEL_COMPUTE_DEVICE | GGML backend strategy | image-gen's CPU/CUDA choice: gpu, cpu, auto. |
LOXEL_DECODE_PROF=1 | std::llm | Per-phase decode profiler (µs buckets: gemv/attn/rope/…). LOXEL_PREFILL_PROF=1 for prefill. |
LOXEL_WORKER_CPU | Dockerfile build arg | CPU tuning for the worker's non-device half; the gpu profile passes native. |
LOXEL_EXTRA_LINK_LIBS | loxel compile link step | How a build image hands its GGML/CUDA libraries to every compile it runs. |
Execution Modes ✅ Working
Three modes, in priority order: the native LLVM compiler is the primary mode (most capable, ships production binaries), the bytecode VM is second (the default for loxel script.lox), and the tree-walking interpreter is third (simplest; useful for prototyping semantics). New features land native-first; a feature existing only in a lower mode is a gap, not a design.
| Mode | Speed | Role | Usage |
|---|---|---|---|
| Native (LLVM) | 50–100× tree-walker | Primary — production binaries; debug builds via -g | loxel compile script.lox -o out |
| Bytecode VM | 18× tree-walker | Secondary — the default interpreted mode | loxel script.lox |
| Tree-walker | Baseline | Third — prototyping / semantics reference | loxel script.lox --tree-walker |
| REPL | N/A | Interactive | loxel or loxel repl |
REPL special commands
Language-Level Events ✅ Working
Event decorators on class methods intercept lifecycle events for AOP, logging, validation, and metaprogramming. All three modes implement six of the seven hooks (@onCall, @onReturn, @onThrow, @onPropertyGet, @onPropertySet, @onInstantiate): they fire through the runtime dispatcher, inherit to subclasses, and a throwing @onPropertySet validation hook aborts the write catchably — verified in the tree-walker too, where both the @onThrow and aborting-write gaps have since been fixed. @onPropertyGet fires natively as of 2026-07-30 (same hook table as @onPropertySet, suppressed inside hooks so a hook reading self.* can't recurse). Only @onInherit remains interpreted-only (native warns at compile time, as it does for @computed). Native nuance: hooks fire on dispatched method calls, not on super.method() fast paths.
Event hook decorators
Built-in method decorators
| Decorator | Trigger | Parameters |
|---|---|---|
@onCall | Before any method call | method_name, args |
@onReturn | After method returns | method_name, result |
@onThrow | Exception thrown | method_name, error |
@onPropertyGet | Property read | property_name |
@onPropertySet | Property written | property_name, new_value |
@onInstantiate | After init() | none |
@onInherit | Class subclassed | child_class |
These class-level hooks take no arguments and fire for every property. To hook a single field — and to see the value it replaced — decorate the field, below.
Per-field hooks — @onInitialize, @onUpdate
A decorator on a field declaration hooks writes to that one field. Like the rest of the family they are named for the event, not for what a handler does with it: @onInitialize fires on the field's first write (typically in init), @onUpdate on every later write. A field may declare either, both, or neither.
The handler receives one event map: e.field (the field's name), e.priorValue (the value being replaced — nil for @onInitialize), and e.newValue.
Rejecting a write is a throw — the same veto @onPropertySet has. There is no return-value convention: a handler that returns normally accepts the write, whatever it returned, and a rejected write leaves the field's previous value in place.
Hooks never fire from inside hooks. A write performed by a handler does not itself fire a handler, so a handler that writes cannot recurse — the same rule the class-level hooks obey.
A handler is evaluated once, at class-definition time, in the class's defining scope, so it is one closure shared by every instance and it has no self. It sees only its event map; reach anything else through a captured variable.
Which to use: @onPropertySet is the broader instrument — it fires for every property, including fields created dynamically by assignment, which a field decorator cannot see because it attaches to a declaration. Per-field hooks are narrower, know which field they guard, and are the only way to see the replaced value.
Unified Object Model ✅ Working
All primitive values behave as objects with methods. Internally they remain efficient (NaN-boxed doubles, bit-pattern booleans) — method calls dispatch directly to C++.
Primitive class contracts: every inline primitive is an instance of its core Loxel class (String, Number, Boolean, Array, Map, Set), and every core class extends Object. The class definitions in stdlib/core/*.lox are the declared contract — including operator methods with typed parameters like def <(other: String). Class methods may delegate to native functions, and the engines fast-path primitive receivers equivalently, but the semantics are the ones written on the class; subclass overrides dispatch through the method table and win. (Single-source build-time embedding of the core classes and typed operator contracts: ✅ landed 2026-07-03.)
Primitive methods
Core-Class Subclassing ✅ Working
Every core class (Number, String, Boolean, Array, Map, Set, and Error — see Error Handling) is subclassable in all three execution modes. A subclass instance wraps its primitive, inherits the core class's methods and typed operator contracts (abs(), <, toString(), …), participates in arithmetic through them, and answers is through its class chain (Meters(3) is Number is true; typeof still reports the boxed kind). Combine with a decorator factory to normalize operands before a method body runs — units-style types in a few lines.
Example — unit-safe arithmetic via a coercion decorator
Abstract Classes 📋 Planned
Abstract classes define partial implementations. Subclasses must implement all abstract methods. Instantiating an abstract class is a runtime error.
Planned syntax
Static Properties & Methods ⚠️ Partial
The static keyword works in the bytecode VM and the native/LLVM compiled backend. static let/static const fields may have an initializer, evaluated once when the class is defined. Both the bytecode VM and the native/LLVM backend enforce visibility and const/readonly mutability on static fields with matching error messages (write-once: the initializer or first assignment succeeds, later writes throw; native errors are catchable, VM errors are fatal). Static members are accessed via the class name, not self.
Static keyword (working)
Static variable: visibility & mutability
Definitions
The words the type system's documentation and diagnostics lean on, and — one paragraph each, no internals — how Loxel achieves them.
Type annotation
A written type on a binding, parameter, return or element (let n: Number, def f(s: String?): Boolean, Array<Number>). In Loxel an annotation is a contract, not a hint: the compiler checks every value that reaches it, and in compiled code a value that provably breaks the promise stops the build. What an annotation asks for is a kind of value — Number means a Number or any subclass of it.
Type inference
Working out a type that was not written: let x = 5 is a Number; a function that only ever returns Strings returns a String. Inferred types are descriptive — assigning something else to an unannotated variable widens it rather than failing — so an annotation is how you ask for the contract instead.
Soundness
A type system is sound when a type it states is never wrong at runtime: if the checker says s is a String at some point, then on every path that reaches that point s holds a String. Soundness is what lets a compiler act on a type — skip a check, unbox a value, call directly — without a safety net underneath. Loxel achieves it four ways. An unknown value (Any) never satisfies a concrete annotation on its own; it has to be narrowed with a checked cast (value :> Number). Classes are nominal: a Vec is a value built by the Vec class or a subclass of it, never something merely shaped like one. Where control paths merge — after an if, at the top and the bottom of a loop — only what every path proves survives. And at every declared boundary — a variable's declaration and each reassignment, a parameter, a return — as well as a cast, the runtime checks the value itself, in all three execution modes; so a declared type holds even where the checker could not see (a value arriving from an untyped module, an Any), and what the compiler specializes on is exactly what the runtime admitted.
Exactness
Knowing a value's exact representation, not just its kind. Because every core type is subclassable (class Meters < Number), a Number may be a plain double or an object wrapping one; Number! says it is the double and nothing else. The exact form is what the native compiler needs to keep a value unboxed and to call an operator directly, so exactness is how an annotated program stays as fast as an unannotated one. Loxel achieves it mostly by inference: a literal is exact, arithmetic on exact operands is exact, a construction Vec(…) is exactly a Vec, and a binding declared Number that currently holds an exact value keeps that proof until something open is assigned. You write T! only where inference cannot cross — a parameter, a field, an element type — and value :> Number! is the checked cast, which throws for a subclass instance.
Narrowing
Refining a type on one path from evidence on that path. A nil test narrows an optional — if (s == nil) { return; } proves s is a String afterwards, and while (node != nil) proves it inside the body and proves it nil after a normal exit; !, && and || compose; and the checked cast :> narrows anything, at the price of a runtime check. Narrowing follows variables, not fields.
Gradual typing, and Any
Types are optional. Any is the type of a value nothing is known about — an unannotated parameter, a field, a value from an untyped module — and it is unknown, not wrong: it flows freely into other Any slots, and into a concrete slot only through :>. A program can be typed one function at a time, and each annotation is checked from the moment it is written.
Contract violation vs. type error
Diagnostics come in two strengths. A contract violation is a provable mismatch — a String assigned to a Number, a call with the wrong number of arguments — and it fails every native compile, whatever the --type-check setting. A type error is a value the checker cannot prove either way, an Any reaching a concrete slot without a cast; it is reported under --type-check=warn and fails the program under --type-check=strict. Both name the value and the promise it fails.
Quick Reference
Feature support matrix across execution modes. Every ✅ row below was re-verified against the current binary in all three modes (2026-09, the #912 sweep); footnoted cells carry the known divergence.
| Feature | Tree-walker | Bytecode VM | LLVM |
|---|---|---|---|
| Variables / const | ✅ | ✅ | ✅ |
| Functions & closures | ✅ | ✅ | ✅ |
| Classes & inheritance | ✅ | ✅ | ✅ |
| Interfaces & traits | ✅ | ✅ | ✅ |
| Generics | ✅ | ✅ | ✅ |
| All 9 pattern types (rest included, #946) | ✅ | ✅ | ✅ |
| Default parameters | ✅ | ✅ | ✅ |
| Named arguments | ✅ | ✅ | ✅ |
| Rest parameters | ✅ | ✅ | ✅ |
| Async / await | ✅ ¹ | ✅ | ✅ |
| i18n support | ✅ | ✅ | ✅ |
| Module system | ✅ | ✅ | ✅ |
| Language events | ✅ | ✅ | ✅ |
| Decorators (user, on methods) | ❌ ² | ✅ | ✅ |
| Unified object model | ✅ | ✅ | ✅ |
| Channels / Thread.spawn | ❌ ³ | ✅ | ✅ |
| Abstract classes | 📋 | 📋 | 📋 |
| Static properties | 📋 ⁴ | ✅ | ✅ |
¹ A top-level await fn() currently runs the async body twice in the tree-walker (#948).
² The tree-walker breaks the unbound receiver convention for user method decorators (#947); built-in method decorators and function/class decorators work in all three modes.
³ Channel/Thread are not callable in the tree-walker (#747).
⁴ static def methods work in the tree-walker; static fields do not exist there yet.
Best Practices
Type annotations
Error handling
Pattern matching vs. if/else
Known Limitations
- Native (LLVM) is the primary mode: production binaries come from
loxel compile; use-gfor Loxel-level backtraces and bounds-checked buffers when debugging (seev2/docs/DEBUGGING.md). The interpreted modes are for iteration and prototyping. - Channels / threads: Implemented (native + VM):
Threadspawn/join andChannelsend/receive work in compiled code; a couple of known concurrency bugs remain in stress scenarios. - Visibility/mutability across modes: Enforced in all three modes (native records per-class field metadata and checks it at every field access). Divergences that remain: VM errors are fatal while native/tree-walker errors are catchable; the tree-walker has no static fields; and for inherited fields the lower modes derive the calling class dynamically (each rejecting a legitimate base-method access pattern that native's lexical resolution allows).
- Abstract classes: Designed but not yet implemented (Phase 19).
- Generics in LLVM: Generic instantiation works, and
Box<Number>is a first-class parameterized-class value (aliasinglet B = Box<Number>; B(x)and named/spread constructor arguments keep the type arguments). Arity/non-generic/bound errors are reported at compile time when the class is statically known; arity and non-generic misuse are also validated at runtime for dynamic class values. Type arguments are erased for execution, but each generic instance records its concrete type arguments, so a method parameter typed as a class type parameter (def set(v: T)) is validated at runtime in all three modes —Box<Number>().set("x")raises the sameType mismatch for parametererror natively, in the VM, and in the tree-walker. Method-level type arguments (obj.method<Number>()— arity, bounds, and parameter validation) and concrete/composite parameter types on generic receivers (v: Number,vs: Array<T>— container kind checked, element types unchecked) are likewise enforced in all three modes. Remaining nuances: native validates method-level type-argument bounds by type NAME (registered class hierarchies and primitives are exact; unresolvable names are accepted conservatively), and element types ofArray<T>/Map<K,V>arguments are not deep-checked in any mode. - Decorators / event hooks: native and the VM implement the full user surface (VM method decorators fixed by #399), and the earlier tree-walker gaps for class decorators,
@onThrow, and the throwing-@onPropertySetabort are resolved;@onPropertyGetnow fires in all three modes. Remaining: tree-walker user method decorators break the unbound receiver convention (#947);@onInherit/@computedstay interpreted-only (native warns). - Tree-walker parity bugs (open): a top-level
await fn()runs the async body twice (#948);Channel/Threadare not callable (#747). (Array rest patterns inmatchwere the inverse case — tree-walker-only — and have been promoted to the VM and native, #946.) - Planned syntax: map comprehensions, do-while,
for awaitover channels. (try-as-expression has shipped — identical in all three modes.)
Comments ✅ Working
Three comment styles:
//single-line,/* */multi-line (nestable), and/** */documentation comments.Example