Q+ 文档

Q+ Language Guide

This guide introduces the Q+ language as implemented by the reference toolchain in Q+ Language 1.5.0-dev and described by the Q+ Language Specification v4.36. It covers the syntax, type system, composite types, functions, generics, traits, macros, FFI, error model, concurrency, the AOT backend, debugging, and the known boundaries of the current implementation.

Version 4.18 focuses on concurrency safety: the memory model, join and spawn semantics, the async scheduler's thread scope, and channel blocking/lifecycle rules are documented under Threads, channels, and thread pools and in specification §9.5.6.

The specification is the normative reference. This guide is a practical companion: it shows complete, runnable examples and calls out behavior that is easy to trip over. Where the specification and the current compiler differ, the difference is stated explicitly.

Every complete program in this guide was compiled and executed with the reference toolchain built by tools/build.sh. Run commands from the repository root so that use std.<module> resolves; from another directory, set QPLUS_ROOT to the repository root.

Contents

A first program

fn main() -> i32 {
    print("Hello, Q+!\n")
    return 0
}

Compile and run it:

build/bqvm --timeout-ms 0 build/qboot.qbc hello.q -o hello.qbc
build/bqvm hello.qbc

The program has three parts:

print accepts integers, floats, booleans, and strings, and works on both the bytecode VM and the ELF AOT backend. The standard library also offers println_int, println_str, println_float, and println_bool in std.io, but that module wraps VM service builtins, so programs that import it cannot be compiled by the ELF AOT backend (see The AOT backend).

On Linux, bqvm currently writes printed lines with CRLF (\r\n) endings; the specification records this as observed behavior.

Program structure

A Q+ source file is a sequence of top-level items:

ItemExample
Module importsuse std.string
Function definitionsfn add(a: i32, b: i32) -> i32 { return a + b }
Async functionsasync fn work(n: i32) -> i32 { return n * 2 }
Struct definitionsstruct Point { x: i32, y: i32 }
Generic struct definitionsstruct Box<T> { v: T }
Enum definitionsenum Color { Red, Green(i32) }
Trait definitionstrait Shape { fn area(self) -> i32 }
Inherent implementationsimpl Point { fn sum(self: Point) -> i32 { ... } }
Trait implementationsimpl Shape for Rect { fn area(self: Rect) -> i32 { ... } }
Macro definitionsmacro square($x: expr) { $x * $x }
Global variableslet limit: i32 = 100
External declarationsextern fn sys_env_get(key: string) -> string
C FFI declarationsextern "C" fn puts(s: string) -> i32

Imports are declared at the top of the file. The specification places extern declarations after all use declarations; follow that order.

Comments use // and run to the end of the line. Semicolons are optional statement separators.

Values, variables, and types

Declarations and assignment

let declares a variable. The type annotation is optional when the initializer determines the type:

fn main() -> i32 {
    let count: i32 = 3
    let label: string = "items"
    let ratio: f64 = 0.75
    let ready: bool = true

    let inferred = 42          // inferred as i32
    let message = "Q+"         // inferred as string

    count = count + 1          // assignment to an existing variable
    print(count); print(" ")
    print(label); print(" ")
    print(ratio); print(" ")
    print(ready); print(" ")
    print(inferred); print(" ")
    print(message); print("\n")
    return 0
}

There is no const or mut keyword; a let binding is writable. Variables are function-local unless declared at the top level (see Globals).

Type names

TypeSpellingDescription
Signed integeri32, i64Wide tagged integers. Arithmetic inside an expression does not wrap or truncate.
Unsigned integeru8, u16, u32, u64Ranges [0,255], [0,65535], [0,2^32-1], and the full 64-bit u64 range [0,2^64-1] on the VM (see Full u64 value range); AOT supports the full A5 u64 path (Stage1+2+3: constants/print/arithmetic/comparisons/casts/f64/containers/ABI/std.u64) with VM/AOT parity; documented loud residuals: type-1 i64 constants >2^60, u64->i64 beyond AOT tagged range, std.io sys_svc, AOT bool print parity, u64 bitwise.u64/containers) stay loud.
Signed small integeri8, i16Ranges [-128,127], [-32768,32767].
Floatf64Double-precision floating point. The specification describes the concept as float; the compiler and standard library use f64.
Booleanbooltrue or false.
StringstringUTF-8 byte sequence backed by a heap object.
Dynamic arrayArray or TElement type T; length is carried at runtime.
Fixed-size array annotationT[N]Element type T and a size annotation N.
Function valuefn or fn(T1, T2) -> RBare fn is a wildcard; a signature restricts parameters and result.
Structdeclared nameNominal composite type; may be generic (Box<i32>).
Enumdeclared nameNominal tagged-union type.
Traitdeclared nameStatic contract only; not a value type.

The lower-case word array is the specification's conceptual name for the array type; the compiler accepts Array or T as an annotation. Writing lower-case array as a type annotation is a type error ([TC11]).

Integer matrix and checked overflow

The small integer types are ordinary types with a range contract. Integer expressions are evaluated exactly in a tagged 64-bit carrier; a value is checked when it is written to a small-integer destination (a let initializer, assignment, return, call argument, struct construction argument, global initializer, or cast):

fn main() -> i32 {
    let a: u8 = 200
    let b: u16 = 60000
    let c: u32 = 4000000000
    let d: u64 = 18446744073709551615
    let e: i8 = 0 - 100
    let f: i16 = 0 - 30000
    print(a); print(" "); print(b); print(" "); print(c); print(" ")
    print(d); print(" "); print(e); print(" "); print(f); print("\n")
    return 0
}
200 60000 4000000000 18446744073709551615 -100 -30000

A literal that cannot fit its annotated small-integer type is a compile-time error, not a silent truncation:

Error: integer literal out of range at line 2 type=u8 range=[0..255] [TC15]
=== Compilation aborted: type errors above ===

A literal above i64 max used where a non-u64 type is expected is also a compile-time [TC15] error (exit status 4); it is never silently truncated.

A value that overflows at runtime aborts with a classified runtime error:

Runtime error: overflow: integer overflow

Exit status is 1. Casts are checked conversions with the same guarantee:

fn main() -> i32 {
    let a: u8 = u8(200)
    let b: i8 = i8(0 - 100)
    let c: u8 = u8(3.9)
    print(a); print(" "); print(b); print(" "); print(c); print("\n")
    return 0
}
200 -100 3

Arithmetic between different integer types is performed exactly and only checked at the write boundary, so let wide: i32 = 255 + 1 is valid and equal to 256. The i32(...)/i64(...)/small-integer casts convert floating-point values by truncating toward zero, and range-check integer conversions.

Full u64 value range (v4.29, A1)

On the VM, u64 supports the full 64-bit range [0, 18446744073709551615]. Full-width decimal literals are lexed exactly (no truncation) and carried as a dedicated unsigned tag. print uses unsigned decimal; comparisons are unsigned (including mixed comparisons with signed integers); and arithmetic in a u64 context uses unsigned wrap-around. Division and modulo by zero abort with Runtime error: division_by_zero: division by zero.

fn main() -> i32 {
    let max: u64 = 18446744073709551615
    let one: u64 = 1
    let zero: u64 = 0
    let wrap_up: u64 = max + one
    let wrap_down: u64 = zero - one
    print(max); print(" ")
    print(wrap_up); print(" ")
    print(wrap_down); print(" ")
    print(max / 3); print(" ")
    print(max % 10); print(" ")
    print(max > one); print("\n")
    return 0
}
18446744073709551615 0 18446744073709551615 6148914691236517205 5 true

Boundaries (the P3-A1 A1 milestone; A2–A5 are follow-ups):

Type inference

An unannotated let takes the type of its initializer. Inference is single-level and fixed at the declaration:

fn main() -> i32 {
    let n = 5                  // i32
    let values = [1, 2, 3]     // i32
    let first = values[0]      // i32
    let name = "Q+"
    print(n + first); print(" ")
    print(name); print("\n")
    return 0
}

Inference covers scalar literals, array literals with a uniform element type, enum and struct construction, the ternary operator, indexing, member access, function-call results, and function references. It does not flow across branches: the inferred type is fixed where the let is declared, and later assignments are checked against it. if and match are statement forms, so let x = if ... and let x = match ... are parse errors.

Globals

A top-level let declares a global variable:

struct Pair { a: i32, b: i32 }

let total: i32 = 10
let values: i32[3] = [1, 2, 3]
let pair: Pair = Pair()

fn main() -> i32 {
    pair.a = 4
    print(total + values[0] + values[1] + values[2] + pair.a); print("\n")
    return 0
}
20

Global scalar variables may omit the initializer and default to zero. Global aggregates — structs, enums, and arrays — must be initialized, otherwise the compiler reports global aggregate requires initializer ([TC12], exit status 4).

Reference semantics

Structs and arrays have reference semantics. Assignment, argument passing, and nested member access share the same underlying object; there is no implicit copy or deep copy. Use an explicit copy (array_copy, or a manual field-by-field copy) when independent data is required.

struct Box { value: i32 }

fn set(p: Box, v: i32) {
    p.value = v
}

fn bump(a: i32) {
    a[0] = a[0] + 100
}

fn main() -> i32 {
    let x: Box = Box()
    x.value = 1
    let y: Box = x
    set(y, 7)
    print(x.value); print(" ")

    let a: i32[2] = [1, 2]
    let b: i32[2] = a
    b[0] = 99
    print(a[0]); print(" ")
    bump(a)
    print(a[0]); print("\n")
    return 0
}
7 99 199

The one exception is nested-function capture, which passes an implicit by-value snapshot (see Functions and closures).

Operators

Q+ expression operators are:

CategoryOperators
Arithmetic+ - * / %
Comparison== != < > <= >=
Logical&& || !
Bitwise& | ^ << >> ~
Conditionalcond ? a : b
String concatenation+

Integer division truncates toward zero, and % is the matching remainder. A division by zero aborts at runtime with Runtime error: division_by_zero: division by zero.

The precedence chain from highest to lowest:

arithmetic (+ - * / %)
  > comparison (< > <= >=)
  > bitwise and (&)
  > bitwise xor (^)
  > bitwise or (|)
  > equality (== !=)
  > logical and (&&)
  > logical or (||)
  > conditional (?:)

For example, c & 1 != 0 parses as (c & 1) != 0. Even though this is correct, an explicit intermediate variable is easier to read:

let lsb: i32 = c & 1
if lsb != 0 {
    // ...
}

Boolean values must not be mixed with integers in bitwise operators. A boolean in a bitwise context contributes zero, so 3 & (5 < 10) evaluates to 0, not 1. Use if on the boolean directly instead.

Strings support + for concatenation and all comparison operators; comparison is byte-wise. The two-argument helper str_concat(a, b) from std.string is equivalent to a + b; there is no variadic form.

The conditional operator is the only expression form of conditional evaluation:

fn main() -> i32 {
    let x: i32 = 7
    let bigger: i32 = x > 5 ? 100 : 200
    let low_bit: i32 = x & 1 != 0 ? 1 : 0
    print(bigger); print(" "); print(low_bit); print("\n")
    return 0
}
100 1

Small integers and signed wide integers are checked by default; unsigned u64 arithmetic in a u64 context wraps modulo 2^64 (see Full u64 value range). For explicit signed wrap-around, std.alg provides wrapping_add, wrapping_sub, and wrapping_mul helpers for u8, u16, u32, i8, i16, i32, i64, and u64 (for example wrapping_add_u8(255, 1) == 0); the i64/u64 helpers wrap modulo 2^56 (A3 follow-up). The default semantics remain checked.

Statements and control flow

The statement forms are:

fn main() -> i32 {
    let i: i32 = 0
    let sum: i32 = 0
    while i < 5 {
        if i % 2 == 0 {
            sum = sum + i
        } else if i == 3 {
            sum = sum + 100
        } else {
            sum = sum + 1
        }
        i = i + 1
    }
    print(sum); print("\n")
    return 0
}
107

if and match are statements, not expressions. They cannot initialize a variable or appear as a function argument, and attempting to do so is a parse error (exit status 2):

// does not compile: parse error (rc=2)
let x: i32 = if true { 1 } else { 2 }

// use the ternary operator instead
let y: i32 = true ? 1 : 2

The specification defines one loop form: while. The current compiler also accepts break, continue, and C-style for (init; cond; step) loops, and the standard library uses break/continue; these are not yet described in the v4.36 specification, so portable code that needs one documented loop construct should prefer while.

Functions and closures

Functions are declared with fn, typed parameters, and an optional return type:

fn add(a: i32, b: i32) -> i32 {
    return a + b
}
fn factorial(n: i32) -> i32 {
    if n <= 1 { return 1 }
    return n * factorial(n - 1)
}

fn twice(x: i32) -> i32 {
    return x * 2
}

fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
    return f(x)
}

fn greet(name: string) {
    print("hello "); print(name); print("\n")
}

fn main() -> i32 {
    greet("Q+")
    print(factorial(5)); print("\n")

    let f: fn(i32) -> i32 = twice
    print(apply(f, 21)); print("\n")

    fn local(x: i32) -> i32 { return x + 1 }
    print(local(41)); print("\n")
    return 0
}
hello Q+
120
42
42

Closures: snapshot capture

A nested function can read an enclosing local or parameter. Captured values are passed to the nested function as implicit by-value arguments at each call site: this is snapshot semantics, not a shared environment.

fn main() -> i32 {
    let x: i32 = 7
    fn inner() -> i32 { return x }
    print(inner()); print("\n")
    return 0
}
7

Writing to a captured name inside the nested function only changes its local copy; the outer variable is unchanged:

fn main() -> i32 {
    let x: i32 = 10
    fn bump() -> i32 { x = x + 1; return x }
    print(bump()); print(" "); print(x); print("\n")
    return 0
}
11 10

Capture works with parameters and inside loops:

fn outer(base: i32) -> i32 {
    let step: i32 = 3
    fn add(a: i32) -> i32 { return a + step + base }
    let i: i32 = 0
    let s: i32 = 0
    while i < 5 { s = add(s); i = i + 1 }
    return s
}

fn main() -> i32 {
    print(outer(2)); print("\n")
    return 0
}
25

The boundary: a capturing nested function cannot be stored in a fn slot or otherwise escape its defining function. The implicit capture arguments only exist at the call sites the compiler can see, so taking its address is a loud type error. There are no heap environments, no delayed-call closures, and no capture across more than one nesting level.

Function values

Functions are first-class values:

struct Ops { f: fn(i32) -> i32 }

let gf: fn(i32) -> i32 = inc

fn inc(x: i32) -> i32 { return x + 1 }
fn apply(f: fn(i32) -> i32, x: i32) -> i32 { return f(x) }

fn main() -> i32 {
    let o: Ops = Ops()
    o.f = inc
    print(gf(41)); print(" ")
    print(o.f(41)); print(" ")
    print(apply(inc, 41)); print("\n")
    return 0
}
42 42 42

External declarations

extern declares a function implemented by the VM runtime rather than in Q+:

use std.io

extern fn sys_env_get(key: string) -> string

fn main() -> i32 {
    println_str(sys_env_get("HOME"))
    return 0
}

The standard library wraps the raw sys_* builtins in ordinary modules (std.env, std.io, std.file, and so on), so application code normally calls the standard-library functions instead of the raw declarations. The ELF AOT backend does not link sys_* builtins (see The AOT backend).

Structs

A struct is a named record with typed fields:

struct Point { x: i32, y: i32 }

impl Point {
    fn sum(self: Point) -> i32 {
        return self.x + self.y
    }
}

fn main() -> i32 {
    let p: Point = Point()      // allocate a struct object
    p.x = 3                     // assign every field before reading it
    p.y = 4
    print(p.sum()); print("\n")
    return 0
}
7

Enums and match

An enum is a tagged union. Variants may carry payloads:

enum Color { Red, Green(i32), Blue(i32, i32) }

fn describe(c: Color) -> i32 {
    match c {
        Color.Red => { return 1 }
        Color.Green(x) => { return x + 10 }
        Color.Blue(a, b) => { return a + b }
    }
    return 0
}

fn main() -> i32 {
    print(describe(Color.Red)); print(" ")
    print(describe(Color.Green(5))); print(" ")
    print(describe(Color.Blue(3, 4))); print("\n")
    return 0
}
1 15 7

Construction and pattern rules:

An enum may have impl methods:

enum Result2 { Ok(i32), Err(string) }

impl Result2 {
    fn is_ok(self: Result2) -> bool {
        match self {
            Result2.Ok(_) => { return true }
            _ => { return false }
        }
    }
}

fn main() -> i32 {
    let good: Result2 = Result2.Ok(42)
    let bad: Result2 = Result2.Err("e")
    print(good.is_ok()); print(" "); print(bad.is_ok()); print("\n")
    return 0
}
1 0

Result/Option are ordinary enums, not built-in exception types. The language has no exceptions; the standard library's std.result module provides a convenient Result encoding (see Errors and exit codes).

Arrays

Q+ has fixed-size annotations, dynamic arrays, and array literals:

use std.array

fn main() -> i32 {
    let a: i32[3] = [10, 20, 30]
    a[1] = 99
    print(a[0] + a[1] + a[2]); print(" ")

    let b: i32 = Array(4)
    b[0] = 7
    print(len(b)); print(" ")

    let m: i32 = [[1, 2], [3, 4]]
    print(m[1][0]); print(" ")

    let copy: Array = array_copy(a)
    copy[0] = 0
    print(a[0]); print("\n")
    return 0
}
139 4 3 10

A heterogeneous literal without an annotation (let a = [1, "two", 3]) becomes a wildcard array and its elements are no longer type-checked. See Known boundaries.

Strings

Strings are UTF-8 byte sequences with a rich standard-library module:

use std.string

fn main() -> i32 {
    let a: string = "Q"
    let b: string = "+"
    let name: string = a + b
    let s: string = "汉字a"

    print(name); print(" ")
    print(str_len(name)); print(" ")
    print(str_char_len(s)); print(" ")
    print(char_code("A", 0)); print(" ")
    print(str_substr("abcdef", 1, 3)); print(" ")
    print(str_char_at(s, 0)); print("\n")
    return 0
}
Q+ 2 3 65 bcd 汉

Modules and the standard library

Standard-library modules are imported by name:

use std.io
use std.math
use std.string

fn main() -> i32 {
    println_float(sqrt(81.0))
    println_int(str_to_int("123"))
    println_str(int_to_str(456))
    return 0
}
9.000000
123
456

Generics

Generics are erased: a single copy of the code runs at runtime, while the type checker binds type parameters at the call site and substitutes them into the result type.

fn id<T>(x: T) -> T { return x }

fn main() -> i32 {
    let n: i32 = id(7)
    let s: string = id("ok")
    print(n); print(" "); print(s); print("\n")
    return 0
}
7 ok

Call-site checks enforce consistency: a parameter that appears twice must bind to the same concrete type, and the substituted return type participates in the surrounding checks:

fn max_of<T>(a: T, b: T) -> T { ... }
let x: i32 = max_of(1, "s")
[TC4] line 3 want=i32 got=string
=== Compilation aborted: type errors above ===

Generic structs are tracked for field typing even though code generation erases the parameter:

struct Box2<T> { v: T }

fn main() -> i32 {
    let a: Box2<i32> = Box2(7)
    let b: Box2<string> = Box2("hi")
    print(a.v); print(" "); print(b.v); print("\n")
    return 0
}
7 hi

Box2<i32>.v = "str" is a [TC2] error and Box2<i32>/Box2<string> values are not interchangeable. An unannotated let b = Box2(7) is inferred as a plain struct and loses field-type tracking (a documented boundary).

Traits

A trait is a static contract. impl Trait for Type must provide every method the trait declares, with compatible arity, parameter types, and return type.

trait Shape { fn area(self) -> i32 }

struct Rect { w: i32, h: i32 }

impl Shape for Rect {
    fn area(self: Rect) -> i32 { return self.w * self.h }
}

fn main() -> i32 {
    let r: Rect = Rect()
    r.w = 6
    r.h = 7
    print(r.area()); print("\n")
    return 0
}
42
  Error: `dyn Trait` trait objects are not supported (static dispatch only; use a concrete type or generic fn) at line 3 [TC11]

Macros

Q+ has declarative macros. A macro is declared with macro name($param: category) { ... } and invoked with name!(args...):

macro square($x: expr) { $x * $x }

fn main() -> i32 {
    print(square!(5)); print(" "); print(square!(2 + 3)); print("\n")
    return 0
}
25 11
  macro twice($s: stmt) { $s; $s; }
  twice!(print("ab"))
  macro swap($a: expr, $b: expr) { let __t: i32 = $a; $a = $b; $b = __t; }

  fn main() -> i32 {
      let __t: i32 = 99
      let x: i32 = 1
      let y: i32 = 2
      swap!(x, y)
      print(__t); print(" "); print(x); print(" "); print(y); print("\n")
      return 0
  }
  99 2 1

FFI: extern "C"

Q+ supports a deliberately small, controlled C FFI:

extern "C" fn puts(s: string) -> i32
extern "C" fn strlen(s: string) -> i32

fn main() -> i32 {
    puts("hello ffi")
    print(strlen("abcd")); print("\n")
    return 0
}
hello ffi
4

Errors and exit codes

Q+ has no exceptions and no try/catch. Errors fall into two groups.

Compile-time errors

The compiler proceeds through lexing/parsing, macro expansion, static type checking, and code generation. Each phase has its own exit status:

Exit statusPhaseTypical output
0SuccessNo errors; a .qbc file is written
1I/O or argument errorUnreadable source, invalid QBC, unknown extern at load time
2Lexical/syntax error (and macro errors)Error: expected token 57 got 7 at line 3, Error: macro: ...
3Semantic or code-generation errorError: undefined function called at line 2
4Static type error[TC1] line 5 want=i32 got=string, [TC15], [TC11]
5AOT unsupported featureAOT_UNSUPPORTED_BUILTIN: ..., AOT_UNSUPPORTED_OPCODE: ..., or AOT_UNSUPPORTED_I64_CONST: ..., written by the AOT tool; no ELF produced

Type errors report the checker rule ([TC1] for let, [TC4] for call arguments, [TC15] for integer literal range, and so on), the source line, and the expected and actual types. Type checking runs before code generation; a type error produces no output file.

Runtime errors

The VM and the ELF AOT backend share one classified runtime-error model. A failure prints a Runtime error: <class>: <detail> line to stdout and exits with status 1:

ClassCause
panicpanic() / panic_i32() from std.panic; prints PANIC: <msg> first
overflowchecked small-integer boundary overflow
division_by_zero / modulo_by_zerointeger or floating-point division/modulo by zero
index_out_of_boundsarray index out of bounds
stack_overflowcall stack overflow
heap_exhausted / out_of_memoryheap object limit / allocation failure
assert_failedassert(cond) with a falsy condition
timeout / ip_out_of_bounds / runtime_errorexecution timeout, malformed QBC, other
use std.result

fn main() -> i32 {
    let r: i32 = Result.Ok(42)
    print(is_ok(r)); print(" "); print(result_unwrap(r)); print("\n")
    return 0
}
1 42

The integer returned by main also becomes the process exit status for normal returns.

Async tasks

The std.alg module provides a minimal cooperative task queue that works on both the VM and the ELF AOT backend:

use std.alg

async fn work(n: i32) -> i32 { return n * 2 }

fn task_a() -> i32 { return work(21) }

fn main() -> i32 {
    let id: i32 = spawn(task_a)
    let r: i32 = await id
    print(r); print("\n")
    return 0
}
42

async fn is a semantic marker (callable like a normal function); await expr is syntax sugar for await_task(expr). spawn(fn() -> i32) enqueues a task, await_task(id) runs queued tasks FIFO to completion, and yield_now() runs one queued task.

The std.async module provides stackful, suspending tasks on the VM (ucontext-based, each task with its own stack and saved interpreter state):

use std.async

fn t1() -> i32 { print("A1 "); yield_now(); print("A2 "); return 1 }
fn t2() -> i32 { print("B1 "); yield_now(); print("B2 "); return 2 }

fn main() -> i32 {
    let a: i32 = spawn(t1)
    let b: i32 = spawn(t2)
    print("await1 ")
    let ra: i32 = await a
    print("ra="); print(ra); print(" ")
    let rb: i32 = await b
    print("rb="); print(rb); print("\n")
    return 0
}
await1 B1 B2 A1 A2 ra=1 rb=2

std.async also provides cancellation, state inspection, timeouts, and real sleeping:

use std.async

fn work() -> i32 {
    print("W1 "); task_sleep(100); print("W2 ")
    return 1
}

fn main() -> i32 {
    let t: i32 = spawn(work)
    let r1: i32 = task_wait_timeout(t, 20)
    let c: i32 = task_cancel(t)
    let st: i32 = task_state(t)
    let r2: i32 = await_task(t)
    print("r1="); print(r1); print(" c="); print(c); print(" st="); print(st); print(" r2="); print(r2); print("\n")
    return 0
}
W1 r1=-2 c=0 st=4 r2=-3

Limits: one VM, at most 16 tasks, each task's operand stack up to 4096 values, no IO-ready queue, and no preemption (tasks yield cooperatively). std.alg and std.async both export same-named wrappers; do not import both in one file. The current compiler has no compiler-generated Future state machine.

The stackful task scheduler is main-thread-only. A worker thread that calls any async primitive (spawn, yield_now, task_wait, task_cancel, task_state, task_wait_timeout, task_sleep) fails loudly with async: tasks are not supported in worker threads (surfaced on stderr with the [thread <tid>] prefix); the VM never performs a cross-thread swapcontext. Use channels and thread primitives for cross-thread coordination.

Threads, channels, and thread pools

std.thread provides a pthread-backed model in which threads never share mutable memory: each thread runs an independent VM instance (copied read-only code/constants, fresh stack, heap, and globals). Communication is only through explicit channel messages.

use std.thread

fn work_a() -> i32 { return 40 }
fn work_b() -> i32 { return 2 }
fn dbl(x: i32) -> i32 { return x * 2 }

fn main() -> i32 {
    let t1: i32 = thread_spawn(work_a)
    let t2: i32 = thread_spawn(work_b)
    let t3: i32 = thread_spawn_arg(dbl, 21)
    let a: i32 = thread_join(t1)
    let b: i32 = thread_join(t2)
    let c: i32 = thread_join(t3)
    print(a + b); print(" "); print(c); print("\n")
    return 0
}
42 42

Channels are bounded queues:

use std.thread

fn prod_a() -> i32 { channel_send(1, 111); return 1 }
fn prod_b() -> i32 { channel_send(1, 222); return 2 }

fn main() -> i32 {
    let ch: i32 = channel_new(4)
    let t1: i32 = thread_spawn(prod_a)
    let t2: i32 = thread_spawn(prod_b)
    let v1: i32 = channel_recv(ch)
    let v2: i32 = channel_recv(ch)
    let a: i32 = thread_join(t1)
    let b: i32 = thread_join(t2)
    print(v1 + v2); print(" "); print(a + b); print("\n")
    return 0
}
333 3

Thread pools run jobs on fixed worker threads, each job in its own child VM:

use std.thread

fn j1() -> i32 { return 10 }
fn j2() -> i32 { return 11 }
fn j3() -> i32 { return 12 }
fn j4() -> i32 { return 9 }

fn main() -> i32 {
    let p: i32 = pool_new(2)
    let a: i32 = pool_submit(p, j1)
    let b: i32 = pool_submit(p, j2)
    let c: i32 = pool_submit(p, j3)
    let d: i32 = pool_submit(p, j4)
    let r: i32 = pool_join(p)
    let s: i32 = pool_job_result(p, a) + pool_job_result(p, b) + pool_job_result(p, c) + pool_job_result(p, d)
    print(r); print(" "); print(s); print("\n")
    let sh: i32 = pool_shutdown(p)
    print(sh); print("\n")
    return 0
}
0 42
0

Caps: 16 threads (ids 1..15 usable), 4 pools, 16 workers per pool, 64 channels per process, channel capacity 64, 256 jobs per pool, 1 MiB per string message. There are no pool_submit_timeout jobs, no job arguments/closures, and no cross-thread structured cancellation of threads. The thread backend requires a host pthread implementation (Linux); the a separate product line kernel backend is separate.

Concurrency memory model and lifecycle (v4.18)

The P2-C10 safety fixes define the following model:

The v4.18 fixes are covered in the reference suite by the concurrency security fixtures under src/tests/threads/security/; TSAN reports zero data races for the thread/channel cases, and ASAN reports zero errors for unjoined-worker teardown cases.

The AOT backend

The build produces build/cgtest.qbc (the AOT compiler) and build/cgtest_aot (the ELF backend). To build a native ELF from a compiled program:

build/bqvm --timeout-ms 0 build/qboot.qbc hello.q -o hello.qbc
build/cgtest_aot hello.qbc hello.elf
chmod +x hello.elf      # the tool writes the file, not its executable bit
./hello.elf

The ELF AOT backend supports: control flow, arithmetic, calls, print, assert, checked integer bounds (INT_CHECK), casts (I2F/F2I/I2I), CALL_INDIRECT for function-value slots, and runtime errors with a stderr traceback (at <fn> (line <N>), up to 8 frames). Its stdout error line and exit status are byte-identical to the VM's. The cooperative task queue in std.alg is AOT-supported.

The backend is loud, never silent, about what it does not support:

FeatureELF AOT behavior
Unknown or unmapped sys_* builtin (for example any program importing std.io)Compile-time loud fail, AOT_UNSUPPORTED_BUILTIN, exit 5, no ELF written
extern "C" FFI shimCompile-time loud fail, exit 5, no ELF written
std.panic / Result unwrap on Err (calls sys_abort)Compile-time loud fail, exit 5, no ELF written
Exponent-notation float constantsCompile-time loud fail, AOT_UNSUPPORTED_OPCODE, exit 5
Integer constants outside [-2^60, 2^60-1] (including negative literals whose magnitude exceeds 2^60-1)Compile-time loud fail, AOT_UNSUPPORTED_I64_CONST, exit 5, no ELF (v4.29)
Programs using unsupported u64 features (A5 Stage2/3: casts/bitwise/std.u64/full containers)Loud fail, AOT_UNSUPPORTED_OPCODE (e.g. code=125), exit 5, no ELF; A5 Stage1 subset builds with VM/AOT parity
Executed std.async task primitiveELF builds; execution traps with Runtime error: runtime_error: AOT: async primitives unsupported (VM-only), exit 1
Executed std.thread/channel/pool primitiveELF builds; execution traps with the same async-primitive message, exit 1
Unused async/thread wrappersDo not block the build (runtime trap provides call-path loudness; compile-time DCE is not implemented)

Because importing std.io pulls in VM service builtins that the AOT backend cannot link, AOT-compatible programs should use the print builtin instead of std.io output helpers. Compile with the VM toolchain for programs that need std.io, std.thread, or other service-backed modules.

Debugging and observability

On a runtime failure, the VM prints the classified error line to stdout and a traceback to stderr:

Runtime error: division_by_zero: division by zero
Traceback (most recent call last):
  at boom (line 1, pc=12)
  at mid (line 2, pc=13)
  at main (line 3, pc=14)

Benchmarks

bench/ contains 9 representative programs (recursion, loops, sieve, sorting, array allocation, strings, function calls, structs, integer math) plus bench/run.sh [reps]. The harness compiles each program, builds an AOT ELF, runs both backends (and two C reference kernels), verifies output checksums, and writes raw per-run timings plus environment/tool anchors to bench/results/raw-*.tsv.

bench/run.sh 3

The suite reports the VM/AOT difference for the machine it runs on. Benchmark numbers are machine-specific and are not comparable across machines; the raw files record the CPU, compiler, and tool hashes so results can be interpreted honestly. Nine of nine benchmark programs are VM/AOT checksum-consistent in this release, and the suite runs as part of tools/test.sh.

Editor and package tooling

Known boundaries

This section summarizes the documented limits of the current implementation. The normative details are in specification §7.4 (type-checker coverage), §7.5 (function values/inference), §8 (semantics, integer matrix, error model), §9.5.6 (concurrency memory model and lifecycle), §9.5.7 (full u64), and §10.1 (AOT integer constant range).

Type-checker coverage and residual limits (spec §7.4)

The checker validates let initializers, assignments, return expressions, call arguments (including extern, impl methods, and method-call syntax), member access, indexing, conditions, arithmetic and string concatenation, unknown type names, match exhaustiveness (TC13/TC14), global initializers (TC12), enum constructor payloads, function-pointer signatures, erased-generic call sites, trait implementation contracts, and small-integer literal/boundary checks (TC15, INT_CHECK).

Intentional or still-open limitations:

Inference boundaries (spec §7.5)

Inference is single-level and fixed at the let declaration. It covers scalar and array literals, enum/struct construction, the ternary operator, indexing, member access, call results, and function references. Because if and match have no expression form, let x = if ... and let x = match ... are parse errors (exit status 2); the only expression-level conditional is ?:.

Heterogeneous array literals follow the documented contract:

Reference semantics (spec §8.1)

Structs and arrays are shared references; there is no implicit copy or deep copy. Nested-function capture is the exception: it is a by-value snapshot (see Functions and closures).

Integer semantics (spec §8.2)

i32/i64 are tagged wide integers: arithmetic does not wrap or truncate (2147483647 + 1 = 2147483648, 70000 * 70000 = 4900000000). The VM supports the full i64 constant range and the full u64 range (v4.29); u64 literals above i64 max require a u64 target and are otherwise [TC15]. Small integers are checked at their type boundaries and abort with overflow rather than truncating. The ELF AOT backend represents integer constants only in [-2^60, 2^60-1]: in-range constants are exact, out-of-range constants are a loud build error (AOT_UNSUPPORTED_I64_CONST), and runtime tagged arithmetic overflow is an A5 residual. Implementing 32-bit hash or bit-mixing algorithms requires explicit masking (for example & 0xFFFFFFFF) or the std.alg wrapping helpers.

Error model (spec §8.3)

Compile-time exit statuses are 0 (success), 1 (I/O), 2 (parse/macro), 3 (semantic/codegen), 4 (type), and 5 (AOT unsupported). Runtime failures abort with status 1 and a classified Runtime error: <class>: <detail> message; there is no runtime recovery or exception handling, and Result is a library convention. Concurrency violations follow the same rule: join/spawn/async-scope violations are classified runtime errors (or [thread <tid>]-prefixed stderr diagnostics when raised inside a worker), never silent failures. AOT output for the error line and exit status is byte-identical to the VM; AOT traceback is supported for non-panic runtime errors.

Concurrency boundaries (spec §9.5.6)

Other hard facts

Reliable patterns

  1. Use if directly on a bool. Write if str_equals(a, b) { ... }, not if str_equals(a, b) == 1.
  2. Import what you call. Add use std.<module> for every standard-library module used in the file, and run from the repository root (or set QPLUS_ROOT).
  3. Put extern after use.
  4. Qualify enum patterns. Use Color.Red in match arms; include _ when not all variants are covered.
  5. Use ?: for expression-level conditions and plain if/match statements elsewhere.
  6. Construct enums with payload values. Pass Result.Ok(5), not a bare integer.
  7. Copy shared data explicitly. Structs and arrays are references; call array_copy when an independent array is needed.
  8. Parenthesize macro arguments when they contain operators.
  9. Choose checked or wrapping arithmetic deliberately. Default small-integer arithmetic is checked; use std.alg wrapping helpers only where wrap-around is intended.
  10. Keep AOT-compatible programs free of VM service modules. Use the print builtin and avoid std.io/std.thread/FFI when targeting ELF AOT; otherwise expect a loud rc=5 or a runtime trap.
  11. Treat threads as isolated VMs. Share data only through channels; string messages are copied at the channel boundary.
  12. Guard division and indexing, because runtime errors cannot be caught.

Next steps

← Q+ Hub