Release Notes
1.18.0
Language
- Signal interception:
sys.onSignal(name, handler)trapsSIGINT,SIGTERM,SIGHUP,SIGQUIT(plusSIGUSR1/SIGUSR2on unix); handlers run isolated, share state viastore, and a handler that callssys.exitruns runtime cleanup before terminating.sys.clearSignal(name)restores default delivery andsys.raise(name)signals the current process.SIGKILLis rejected.
Bundling
- Built binaries answer standard first-argument flags:
--help,--version(application name and version fromgeblang.yamlplus the engine version), and--noticesfor the embedded third-party licence text.--passes everything after it to the application untouched.
Performance
- The bytecode VM shares once-prepared chunk metadata and constant pools across VM instances and recycles both wrapper and cross-module-call VMs through escape-guarded pools. Static class members move to a synchronized overlay (semantics unchanged) so the constant pool can be shared untouched. Server workloads that dispatch through callbacks and cross-module calls speed up substantially: a representative typed web route serves ~9x more requests per second than 1.17.0, with median latency cut by ~90%, and VM-mode serving is now the fastest deployment path.
HTTP server
http.serveandhttp.listenacceptopts.maxBodyBytesto cap the request body; oversize requests are answered with 413 before the handler runs.
Fixes
crypt.jwtVerifywithoutopts.allowedAlgspins the accepted algorithm family to the key type (raw secret: HS only; RSA public key: RS only; EC: ES only; Ed25519: EdDSA only). This closes the algorithm-confusion forgery where an HS token minted with a public PEM as the HMAC secret verified against that PEM. Passopts.allowedAlgsto widen or narrow the policy explicitly.- Dict spread works on callable values on the bytecode VM, matching
the evaluator:
let g = f; g(...{"q": "x"})binds named arguments (and engages declared defaults) for function values, lambdas, closures, and reflection-obtained callables. io.existsreturnsfalseinstead of throwingIOErrorwhen a path component is a regular file (/some/file.txt/child). An existence predicate never throws for a path that cannot exist; genuine faults (such as permission errors) still throw.
1.17.0
Language
- Default parameters combine correctly with a variadic parameter. A
signature like
f(int a, int b = 10, int ...rest)now binds in every context (plain function, lambda, method, static method, constructor) and call shape (positional with the default engaged, named arguments, spread). Previously the bytecode VM rejected valid calls at compile time and the evaluator could crash on named arguments or method calls. - A variadic parameter is typed as
list<T>inside the function body, so list methods (rest.length(),parts.join(sep)) type-check. Previously the analyzer treatedint ...restas a bareint, which rejected valid code at top level and broke module compilation on the bytecode VM. - Spread arguments now work in every dispatch context on both backends:
constructors, instance methods, and static methods accept
...listand...dict(extra dict keys are dropped, matching plain functions), including mixed positional-plus-spread calls. - Defining a single ordering dunder enables all four comparison
operators:
a > bderives fromb.__lt(a)(anda < bfromb.__gt(a));<=and>=negate the strict comparison when the direct dunder is missing. A defined dunder always wins over a derived one. range.firstandrange.lastwork in field form alongsiderange.length, matching the documented surface; empty ranges yieldnull.
Fixes
- An exception thrown inside a generator keeps its class in the
consuming loop on the bytecode VM, so
catch (ValueError e)matches it (including subclasses and comprehension consumption). Previously the VM collapsed it into a generic runtime error. - The evaluator derives
<=and>=from a lone__gt/__ltthe same way the bytecode VM always has, removing a latent divergence.
Performance
- The bytecode VM dispatch loop fetches instructions by pointer instead of copying them: integer-loop and arithmetic benchmarks improve by roughly 7-15% and recursive call workloads by ~18%.
- Numeric literals are parsed once and cached on the syntax tree instead
of re-parsed on every evaluation. Allocations in call-heavy evaluator
workloads drop by about 23%, which speeds up
geblang testruns.
Testing
- Same-module test files: a
*_test.gbthat declares the same module name as a sibling module file runs inside that module undergeblang test, so private functions, classes, constants, and module state are directly testable without exporting them (the Go same-package test convention).geblang checkand the editor understand the convention; outside the test runner private members stay private. See the testing chapter for details.
Tooling
- The parity fuzzer generates random required/default/variadic signatures called positionally, with named arguments, and with spread across all dispatch contexts, plus generators that throw typed errors - locking the new behavior against backend drift.
1.16.0
Breaking: in-place collection mutators
- List mutators now mutate the receiver and return it instead of
allocating a copy:
push,pop,prepend,unshift,insert,removeAt,remove,reverse,sort, andsortBy. Accumulation loops are amortised O(1) per element;xs = xs.push(v)keeps working (the reassignment is now a no-op). Code that relied on the receiver being left unchanged must take a copy:reversed()andsorted()are the copy variants, andcopy()/deepCopy()cover the rest (xs.copy().sortBy(...)). set.addandset.removemutate in place and return the receiver.- All in-place mutators raise
ImmutableErroron frozen receivers, and the growth methods enforce the declared element type (TypeError), including the compiler's fusedxs = xs.push(v)fast path, which previously skipped both checks. pop()returns the receiver, not the removed element; uselast()beforepop()to peek.- A function value now satisfies a
callableelement type in list growth checks (list<callable>.push(fn)).
Language
for-inloops and comprehensions now iterate dicts, sets, and strings directly on both backends. Dicts yield insertion-ordered[key, value]pairs (destructurable into two binders:for (k, v in d)), sets yield elements in their sortedtoList()order, and strings yield single-character strings matching.chars(). Previously dict iteration worked only on the evaluator'sfor-in, set iteration only in evaluator comprehensions, and string iteration not at all.- List patterns in
matchaccept literal elements alongside binders:case ["go", n] if (n > 10) => ...pins positions by equality and captures the rest. Numbers (including negatives), strings, bools, andnullwork as literal elements. - Arrow-bodied arms in match STATEMENTS now execute their action
expression. Previously
match (cmd) { case "serve" => startServer(); }- the documented action form - matched the case and silently did
nothing on both backends; only the
case X:block form ran.
- the documented action form - matched the case and silently did
nothing on both backends; only the
- Generic function call-site inference projects through to constructed
instances:
make("hello")forfunc make<T>(T v): Pair<T, T>reportsreflect.typeBindingsof{"A": "string", "B": "string"}instead of the bare type-parameter name. The bytecode VM already behaved this way; the evaluator now matches.
HTTP
- The request builder gains
withBodyFile(path): the file streams from disk as the request body withContent-Lengthtaken from the file size, so large uploads never load into memory.withBody,withJson, andwithBodyFilereplace each other.
Reflection
reflect.functionnow resolves native module functions by qualified name (reflect.function("math.sqrt")) to a first-class callable, the same value the baremath.sqrtexpression produces. Import aliases work (import math as mresolves"m.sqrt"); unknown members returnnull.reflect.functionaccepts a native function value directly on both backends, and structural introspection (reflect.parameters,reflect.location,reflect.returnType) degrades gracefully for native functions (empty parameters,nulllocation,voidreturn) instead of raising on the bytecode VM.reflect.moduleresolves pure native modules even in loader-less embeddings of the VM.- All
reflect.*functions dispatch withoutimport reflecton the evaluator, matching the VM (previously onlyreflect.function,reflect.class,reflect.module, andreflect.classeswere ambient there;reflect.parametersand the rest required the import).
Fixes
- Calling an undeclared bare name (for example a misspelled function or a
non-existent error class) is now a static error on the evaluator path
too, matching the bytecode compiler. Previously
geblang run --disable-vmandgeblang testran such programs until the bad call, which could be silently swallowed bytry/catch. - A literal division or modulo by zero (
5 // 0,5 % 0) is no longer a hard compile error on the bytecode VM. It throws a catchable runtime error on both backends, matching evaluator semantics;geblang checkflags it withwarning[div-by-zero]. - The bytecode VM's integer fast paths reported "integer division by zero" for a modulo by zero; they now say "modulo by zero" like the evaluator and the generic path.
- Runtime error messages for unknown primitive methods and unsupported binary operands are now identical on both backends ("unknown method set.bogus"; "unsupported operands for -: string and int"). The VM previously said "set has no method bogus" and the misleading "left operand must be numeric".
awaitnow rethrows the error raised inside an async function with its original class and message on the bytecode VM, so typed catch clauses (catch (ValueError e)) match it. Previously the VM collapsed the failure into a generic runtime error that only a basecatch (Error e)could see, with a mangled message. The evaluator already behaved correctly; both backends now agree. This also applies totask.await()andasync.await(task).
1.15.0
Standard library
datetime.Instantis the canonical datetime object: construct it from calendar components (datetime.Instant(2024, 1, 15)), the current time (datetime.Instant()), a unix timestamp, or an RFC3339 string; copy it with.copy(); and list its methods withdir. It is immutable - every operation returns a new instant. The unused interpreter-onlyDateTimereference class was removed; usedatetime.Instant.
Fixes
- Deep-cloning a module value now preserves the module's canonical identity.
Previously, contexts that deep-clone a captured environment (per-request
request handlers,
clone.deep, goroutine isolation) dropped it, so a native function called through an aliased import (for example a module imported under a short alias) could resolve against the wrong module and fail with an "unknown function" error. Cloned handlers now resolve aliased native calls correctly. - Using a module name as a value (assigning it to a variable, passing it to a
function, returning it, or storing it in a collection) is now a clear
compile-time error on both backends. Reference a module's members with
module.memberor alias the import withimport module as name. Module introspection by name usesreflect.module("name")(a string). dir(<module>)now works on both backends (it previously failed to compile a bare module name on the bytecode backend). Under the evaluator it lists a module's full accessible member surface.reflect.module,reflect.class, andreflect.functionnow resolve imported native modules consistently on both backends. Previously the bytecode backend returned null forreflect.module("<native>")and failed to compilereflect.class/reflect.functionover a native module; both now match the interpreter (a native module's class exports are reflectable; native functions remain non-reflectable and return null).- Reflection over imported user-module members is now consistent on both
backends.
reflect.location(target).modulereports the declaring module's canonical name on the interpreter (it previously left it blank);reflect.function("name")resolves a function exported by an imported module by bare name on the interpreter (it previously returned null); andreflect.locationof a qualified-name class lookup (reflect.class("module.Class")) reports the declaring module and source position on the bytecode backend (it previously returned null).
1.14.0
Standard library
- New profiling context managers in the
profilermodule.profiler.timer()brackets a block withwithand reports wall-clock time afterwards (elapsedMs()at microsecond precision,elapsedNs());profiler.profile()additionally captures CPU and memory (elapsedMs(),cpuMs(),heapBytes(),allocs(),gcCount(), and a fullreport()dict). Create the object, run the work insidewith (...), then read the results from the object after the block. - New
time.monotonicNs(): a monotonic nanosecond clock for high-resolution duration measurement. The existingtime.monotonic()is millisecond-grained.
Tooling
- The editor now offers completion and hover for every standard-library module written in Geblang, including their classes and methods. Many source-backed modules (for example the async primitives, the string builder, and others) were previously absent from completion.
Fixes
- Context managers now run
__enterand__exitfor an object whose class is defined in an imported module when running compiled bytecode. These were previously skipped on that path, so awithblock over an imported class could leave its setup or cleanup unrun.
1.13.0
Standard library
- New
imagemodule: a portable, native raster-image toolkit that needs no system library. Decode PNG / JPEG / GIF / WebP from a file or bytes, create blank images, and transform via resize (high-quality resampling), crop, and 90-degree rotation. Encode back to PNG / JPEG / GIF. Each transform returns a new image; the source is left unchanged. Released through theImageclass plusimage.load,image.loadBytes, andimage.blank. - New
clib.zstdmodule: Zstandard compression over the system libzstd, withcompress(data, level = 3)anddecompress(frame). - New
clib.magicmodule: content-based file type and MIME detection over the system libmagic, viadetect(path),mime(path), and aMagicclass for reuse and buffer input. - New
clib.systemdmodule (Linux): the sd_notify readiness protocol (ready,watchdog,status, and rawnotify) and structured journald logging (journal), over the system libsystemd. - New
clib.cursesmodule: a full-screen terminal UI surface over ncurses (screen lifecycle, cursor movement and output, key input, colour pairs, and text attributes). Single-owner: drive it from one task.
The clib.* modules load system shared libraries through the in-process FFI, so
they require FFI to be enabled in geblang.yaml (or --allow-ffi). Each is
safe to call from any async task except where its docs note a per-handle lock or
a single-owner constraint.
Type checking
- Static type checking now runs on the compile path on both runtimes, not only
in
geblang check. A type error is reported before execution bygeblang run,geblang test, andgeblang buildalike, and the two runtimes agree on what they reject. - Type checking now extends into class method and constructor bodies. Argument
types, member access on
this.fieldand on typed locals, andreturnexpressions are validated inside methods, not only in top-level and free-function code. The checks reach across module boundaries: a call on a class inherited from another module is validated against the inherited signature. - Unknown type names in annotations are flagged. A bare type name used in any
annotation position (parameter, return, field, variable, generic argument,
nullable, union, catch clause, or
ascast) that resolves to no known type (primitive, declared class, interface, enum, type alias, in-scope generic type param, or built-in error class) is an error at bothgeblang checkand compile time, so a typo in a type hint is caught before it runs. A module-qualified type name whose module does not export that name is flagged bygeblang check. - Type-mismatch errors are clearer. A failed call now names the specific
parameter and the expected and actual types (for example,
g expects int for parameter 'x', got string) rather than a generic "no matching overload". When an unknown-type error already explains a bad signature, the redundant follow-on error is suppressed.
Language
- A class imported with
from module import Namecan now be used directly as a parent class, the same as the qualifiedextends module.Nameform. - Cross-module inheritance now behaves identically on both runtimes in every
position. Calling an inherited method, reading or writing an inherited field,
instanceof, static members, and interface default methods all resolve correctly when a class extends a class, or implements an interface, declared in another module, including through a local intermediate subclass and across multi-level chains. - An interface default method is now available on a subclass of the implementing class on both runtimes.
Fixes
- Multi-level
parent.method()chains now resolve to the correct ancestor on both runtimes. A method that callsparent.method()where that ancestor also callsparent.method()no longer recurses on itself. - Several type-checker false positives were corrected: passing
nullto ananyparameter, assigningnullto a nullable variable after a non-null narrowing, and returning or assigning a value to a generic type parameter are no longer wrongly rejected. - The JSON, XML, CSV, and YAML streaming readers, and the in-memory IO buffer,
stream, and capture objects, now work on the bytecode runtime (
geblang runandgeblang build), matching the evaluator.reader.next()andreader.hasNext()produce the same event stream on both.
Documentation
- The internals reference now documents how the FFI is implemented and its
threading and thread-safety model: FFI calls run on the calling goroutine's OS
thread, native library state is the caller's responsibility, and
errnois valid only immediately after a call. - The language and standard-library reference received a broad accuracy pass:
corrected examples and signatures, a worked cross-module inheritance example,
the
ImmutableErrorbuilt-in, the JSON streaming-reader API, and the current scope ofgeblang check.
1.12.0
Classes
- Fields can be declared set-once with a field-level
@immutabledecorator. A set-once field is writable while the constructor runs and locked afterwards; a later assignment raisesImmutableError, while other fields stay mutable. The lock is inherited by subclasses. An@immutablefield may not declare a default value. - A class that defines
__stringis now rendered through it by string interpolation ("${x}"),io.println, andio.print, not only by an explicitas stringcast. Classes without__stringkeep the default inspection form. - New
@dataclassdecorator generates a constructor, value-based equality, a__stringrendering, and awith(...)copy helper from a class's declared fields.@dataclass(frozen: true)also makes instances immutable. Any member written by hand overrides the generated one. Operates on the class's own fields; a data class that extends another class declares its own constructor. - Frozen instances (whole-class
@immutableor@dataclass(frozen: true)) are now usable as dict keys and set members by value: two frozen instances with equal fields are the same key. Mutable instances continue to key by identity. - New
@overridedecorator asserts that a method overrides an ancestor method; a method marked@overridethat overrides nothing is a compile-time error. The check is by name and skips parents the analyzer cannot resolve. - New
@deprecated("message")decorator marks a function, method, or class for removal.geblang checkreports every use site aswarning[deprecated]with the optional message. Advisory only; it never changes whether code runs. - New
@memoizedecorator caches a top-level function's result by its arguments (unbounded, per-process); recursion through the function's own name is memoized too. Applying it to a method, async function, generator, or void function is a compile-time error.
Fixes
-
Field-level
@immutableis now enforced across module boundaries: a subclass inheriting a set-once field from a parent in another module can no longer mutate it after the parent constructor runs. The field locks when its declaring constructor completes, on both runtimes. -
__stringis now applied consistently across runtimes for implicit string rendering; previously string interpolation rendered an instance through__stringunder one runtime but used the default inspection form under the other. -
Whole-class
@immutableis now preserved through the bytecode cache. It was dropped when a class's compiled chunk was loaded from a.gbccache or a built binary (the class immutability flag was not serialized), so a frozen instance could be mutated on a second run. Class immutability metadata now round-trips through encode/decode. The bytecode format version increments, so stale caches recompile automatically.
1.11.0
Bundling
geblang.yamlgains aresources:list.geblang buildembeds the listed files (directories are embedded recursively; glob patterns match files) into the bundle at their project-relative path, so a built binary can ship templates, static assets, and data files. A pattern that matches nothing is a build error.geblang build --resource <path>embeds additional resources beyond the manifest list;--resource <path>=<bundlePath>remaps a resource's bundle location, so a build step can embed processed copies without altering the source tree.- New
sys.bundleDir()returns the extract directory of a built binary's embedded resources, or""when not running from a bundle. Resolve resource paths against it (falling back to the project directory when empty) so the same code reads its files in development and in a built binary. geblang buildnow embeds source-backed standard-library modules (such asasync.sync) that are also natively registered. Previously these were skipped as native and left out of the bundle, so a built binary that used them failed at runtime. Modules with no source remain provided by the runtime binary.
Running
- Running a file directly (
geblang <file>) now auto-invokes an exported top-levelmainwhen one is declared: amodulethatexport func main(list <string> args)runs the same whether executed directly or built withgeblang build. Command-line arguments are forwarded tomain, and anintreturn value becomes the process exit code. Files with no exportedmainrun as scripts exactly as before.
Fixes
- Fixed a bytecode-VM bug where a closure created inside a
fororwhileloop captured variables incorrectly: a closure that returned could crash, and aletdeclared in the loop body was shared across iterations instead of being a fresh binding. The VM now matches the evaluator exactly - a loop-bodyletis a fresh binding each iteration (closures stored per iteration keep their own value), the loop variable itself is a single shared binding, and assignment to a captured variable is visible through every closure that captured it.
1.10.0
Numeric types
- Mixed-type numeric operations now follow one consistent, precision-safe rule
on both backends.
intandfloatmix in arithmetic by promoting the int to float (3 + 2.5fis5.5).decimalandfloatstill may not mix in arithmetic (adecimalis exact, afloatis not), so2.5 + 2.5fis a clear error - convert explicitly.intanddecimalcontinue to mix exactly. - Comparisons (
== != < > <= >=) and membership (in,.contains()) now work across all numeric types and compare by exact value. Previously3 == 3.0freturnedfalse(comparing types, not values) and3 < 2.5fwas an error; now3 == 3.0fistrue,2.5 == 2.5fistrue, and0.1 == 0.1fisfalse(the binary float is genuinely not one tenth). No precision is lost and comparisons never error on numeric operands.
Math
- New
math.lerp(a, b, t)(linear interpolation,a + (b - a) * t) andmath.remap(x, inLow, inHigh, outLow, outHigh)(linearly remapxfrom one range onto another). Both preserve precision:int/decimalinputs compute exactly and return adecimal,floatinputs returnfloat, and mixingfloatwithint/decimalis an error (matching the arithmetic operators).remaperrors on a zero-width input range. Neither clamps. Useful for precision-safe interpolation over lookup tables (fee schedules, rate bands).
Modules
- Built-in module names (every native and stdlib module) are now reserved: a
program or package module may not declare one of these names, and a built-in
name always resolves to the built-in, identically on the evaluator and the
bytecode VM. This removes a divergence where a local source file could shadow
a built-in on one backend but not the other. The reservation is on the
declared module name, not the filename, so a namespaced module (e.g.
module myapp.errors;in a file namederrors.gb) is unaffected. A collision is reported bygeblangandgeblang check. - New reserved
geblang.import prefix:import geblang.jsonresolves explicitly and unambiguously to the built-in module, regardless of local files. Thegeblang.*namespace is reserved for built-ins.
AI and retrieval
- New
PgVectorStore(invectorstore): a Postgres + pgvector backend behind the existingVectorStoreinterface. Uses a typedvector(D)column, a metric-matched HNSW index, andjsonbmetadata, with index-using approximate nearest-neighbour queries - the same shape as idiomatic pgvector usage. Built on thedbmodule; no new dependency. The extension, table, and index are created on construction;addupserts by id. - New
HnswVectorStore(invectorstore): an in-process HNSW index for sublinear approximate-nearest-neighbour search with no external service - the middle ground between the exact O(n) in-memory store and a database backend. Tune recall withmandefSearch. Behind the sameVectorStoreinterface. - New
searchFilter(query, k, criteria)on every vector store: a portable, dict-based metadata filter ({"field": value}for equality; nested{"field": {"gte": x}}forgt/gte/lt/lte/ne/in). In-memory and SQLite stores apply it in process;PgVectorStorepushes it down to SQL as jsonb containment and range predicates. The callablesearchWhereremains for arbitrary in-process predicates.
Testing
- Tests can now be skipped.
this.skip(reason)skips at runtime (for conditional cases such as an integration test that needs a service or environment variable); the@Skip/@Skip("reason")decorator skips a method unconditionally. Skips count separately from passed/failed, do not affect the exit code, and appear in the summary (skipped=N) and asSKIPlines under--format verbose.test.run()results gain askippedcount.
1.9.0
Tooling
geblang checknow flags method calls whose arguments match no overload - wrong argument type, wrong element type (e.g.list<int>into alist<string>parameter), or wrong arity - the way both backends already reject them at runtime. Previously only free-function calls were validated; method calls were unchecked. The check stays conservative (silent on generic parameter positions, untyped parameters, variadic overloads, or unknown argument types) to avoid false positives.
Collections
- New
seqmodule:seq.stream(source)wraps any iterable (list, set, range, generator) in a lazy, single-use fluent pipeline. Intermediate operations (map,filter,flatMap,take,drop,takeWhile,dropWhile,distinct,peek,sorted,sortedBy) build a generator chain and run nothing until a terminal operation (toList,toSet,forEach,count,reduce,first,firstOr,find,any,all,none,sum,min,max,join) pulls values through once, so no intermediate lists are materialised and huge or unbounded sources stay cheap. The lazy counterpart to the eagercollectionsmodule and the built-in list methods.
AI and retrieval
- New
vectorstoremodule: stores(id, vector, metadata)records and ranks them by similarity (cosine, dot, or euclidean).MemoryVectorStoreis a mutex-guarded brute-force in-memory store;SqliteVectorStorepersists vectors as float32 BLOBs through thedbmodule (table auto-created, upsert by id). Both share aVectorStoreinterface withadd,addAll,get,delete,search,searchWhere(metadata-filtered),count, andclear. Vectors are stored packed as float32 and scored by the nativevecmathkernel, keeping search off the interpreted path. - New
vecmathmodule: float32 similarity kernels -score(metric, a, b)and a batchedtopK(vectors, query, k, metric)- over vectors given as lists or packed float32 blobs. - New
ragmodule: retrieval-augmented-generation helpers on top ofvectorstore.chunksplits text into overlapping windows (by words, characters, or paragraphs);indexchunks, embeds, and stores a document;retrievereturns the most similar chunks for a query;contextassembles them into a prompt-ready block. Built on a smallEmbedderinterface (with anLlmEmbedderadapter) so it stays provider-agnostic and testable without a network.
Fixes
collections.sortBy(list, selector, descending)now accepts the optionaldescendingflag through the module-function form, matching the list-method form and both backends.- Database parameter binding now accepts plain integers and
bytesvalues (BLOBs); query results now decode the full range of integer and float column types returned by the supported drivers. - Spreading a list into a native function's variadic parameter (
f(...list)) now expands correctly when running on the bytecode VM, matching the evaluator.
1.8.0
Dict-like objects
- Classes can opt into subscript syntax with the
__index(key)and__setIndex(key, value)magic methods, soobj[key]andobj[key] = valuedispatch to the class. - New
inmembership operator:key in collectionreturns a bool for lists (element), dicts (key), sets, strings (substring), and ranges, and dispatches to__contains(key)on user objects. Negate with!; a range literal needs parentheses (x in (1..10)). Thefor x in collectionloop is unchanged. - New
maps.DictInterfacestdlib interface: implement__index+keys(and optional__setIndex) to inheritcontains,get,values,length,isEmpty, and__contains(soinworks) as defaults.
HTTP client
- HTTP client calls (
http.get,http.post,http.request, the request builder'ssend, the client methods, andfetchAll) now return a richResponseobject with reader methods:status(),ok(),text(),bytes(),json(),body()(the raw body value, the method form ofresp["body"]),header(name),headers(), plus the status predicatesisSuccessful(),isRedirect(),isClientError(),isServerError(), andisNotFound(). - The
Responseobject stays index-compatible with the previous dict shape:resp["status"],resp["body"], andresp["headers"]still work, andresp.toDict()returns the plain dict. Existing code keeps working unchanged. - New immutable request builder:
http.request(url)(one argument) starts a fluent builder withwithMethod,withHeader,withHeaders,withQuery,withBody,withJson,withBearer,withBasicAuth,withTimeout, andsend. EachwithXreturns a fresh builder, so a base builder can be reused for several requests without leaking state. - New
http.getAll(urls, {limit})performs parallel GETs and returns a list of Responses in input order.http.fetchAllnow accepts request builders as well as spec dicts, and both take an optional{limit}to cap concurrency. A configured client'sfetchAllgains the same options. - In a parallel batch, a request that never completes a round trip (DNS
failure, connection refused, timeout) is reported as a
ResponsewithisError()true and the message inerror()(status0), so the result list is uniform and one failure does not abort the batch.resp["error"]still returns the message.
HTTP server
- Handlers can opt into a rich
Requestobject by declaring the parameter asRequest(the plain request-dict handler stays the default). The object addsscheme(),isSecure(),host(),clientIp(),isMethod(name),isJson(),text(),cookie(name), typed query gettersquery,queryInt,queryBool,queryAll, and route-parameter accessorsrouteParam(name)/routeParams()(route params also appear intoDict()). Object handlers now also work under the bytecode VM, not just the evaluator. - New
http.redirect(url, status=302)builder returns aResponsewith theLocationheader set; it shares the status predicates with all responses. - Plain request-dict handlers now also receive the proxy-aware
scheme,host, andclientIpkeys (resolved like theRequestaccessors, honoringtrustedProxies), plus_clientCertfor a verified mTLS peer. - New
trustedProxiesserver option (a list of IPs/CIDRs, or the keyword"private"). When the immediate peer is trusted,clientIp()is taken fromX-Forwarded-For,scheme()/isSecure()fromX-Forwarded-Proto, andhost()fromX-Forwarded-Host. Otherwise the forwarded headers are ignored so a client cannot spoof its address. - Mutual TLS: a server
tlsblock acceptsclientCa(PEM CA pool) andclientAuth("require"or"optional") to verify client certificates. A richRequestexposes the verified peer certificate viaclientCert()(subject,issuer,serialNumber,notBefore,notAfter,dnsNames, or null). Outbound client certificates (tls.clientCert/clientKey) were already supported. - Connection-level server errors (TLS handshake failures, malformed requests)
are now quiet by default instead of being written to standard error. Pass an
onErrorcallback in the server options to observe them; it receives one message string per failure. These happen before any handler and cannot be caught as Geblang errors, so the callback is the way to log or count them. - Automatic certificates: a server
tlsblock acceptsautoCert(a host or list of hosts), with optionalautoCertCacheDirandautoCertEmail, to obtain and renew ACME (Let's Encrypt) certificates via the TLS-ALPN-01 challenge on the same listener. HTTPS servers also negotiate HTTP/2 automatically. web.routerhandlers and middleware can opt into the richRequestandResponseobjects by declaring those parameter types (an after-middleware's second parameter typedResponsereceives one), and may return aResponse. This is opt-in by type;dict<string, any>handlers and middleware are unchanged.- Server request handlers (
http.serve,http.listen,net.serve) now run with per-request isolated state on both the evaluator and the bytecode VM: a handler's mutations to captured state are private to that request and cannot race a concurrent request, so a handler that touches a captured value no longer risks a crash under concurrent load. Such state does not persist across requests - share cross-request state through a thread-safe handle (database, cache, key-value store) rather than a captured mutable container.
Encoding and templates
- New
encoding.sanitizeHtml(html)sanitizes untrusted HTML against a safe allow-list (keeps common formatting tags, strips scripts/styles andon*event handlers) - for rendering user-submitted HTML. This complementsencoding.htmlEscape, which neutralizes all markup. encoding.base64Encodenow accepts astringorbytes(matching the other base encoders).- Breaking:
encoding.base64UrlDecodenow returns astring(consistent withencoding.base64Decode), so URL-safe Base64 round-trips strings without a manual conversion. For binary, decode withbytes.fromBase64Url(orbytes.fromBase64), which returnbytes. - The
templatemodule reference now documents the full engine (data binding,if/range/with, pipelines,Engine/load/Template.render) and its contextual auto-escaping.
Concurrency
- New
sys.goroutineId(): returns the current goroutine's id (positive, stable within a goroutine, unique among live goroutines). An advanced primitive for goroutine-local / request-scoped state, e.g. keying astore.Storeby it. - New
store.Store: a thread-safe shared key-value store for state shared across concurrent tasks or request handlers. Every operation is serialised internally and values are deep-copied in and out (isolated snapshots), with atomicincr,getOrSet,compareAndSet, andupdate(key, fn). Sharing a plain dict/list across goroutines is unsafe; reach for aStorewhenever you need a shared mutable map. A lower-level functional API (store.new(),store.get(h, key), ...) backs the class.
Other
typeof(x)can now be compared to a type name string:typeof(x) == "int"andtypeof(obj) == "Response"work as expected.typeofstill returns a type value, sotypeof(x) == intkeeps working too.geblang buildnow writes a<output-path>.NOTICES.txtsidecar with the third-party attribution notices for the components the binary embeds, so a distributed binary stays licence-compliant. It is a sidecar file, not a built-in flag, so it never clashes with alicensesargument the built program may define.geblang checknow flags collection element-type mismatches that only the runtime caught before, e.g. passing alist<int>where alist<string>is expected. Built-in collections stay covariant, so alist<Dog>into alist<Animal>parameter and any collection intolist<any>remain clean; only genuinely unrelated element types are reported.sockets.serve(host, port, handler)now hands the callback a typedSocket(the same typesockets.dialreturns:for (line in conn),readLine,writeln,close,localAddr/remoteAddr) instead of a raw{handle, stream, ...}dict. Breaking: a handler typeddict<string, any>that readraw["stream"]should now take asockets.Socketand use it directly.
Fixes
for (x in obj)whereobj.__iter()returns another object that itself needs iterator resolution (for example an object whose__iterreturns a second iterable object, or a stream) now follows the chain on the tree-walking evaluator, matching the bytecode VM. Previously the evaluator stopped at the first hop and reported the inner object as "not iterable".- Interface default methods now resolve correctly when invoked outside the
method-call path (the
inoperator, subscript access, serialization, reflection) and across module boundaries: a cross-module interface default can call sibling default methods onthis. Previously these silently failed to find interface-default implementations. - HTTP handlers can now use app-global handles created at setup. A handler runs in a callback evaluator, and web-app, http-client, and cookie-jar lookups now resolve through the parent (db and logger handles already did), so an app with routes registered up front serves correctly over a real socket instead of failing with "unknown web app handle". Handle ids created inside a handler stay isolated to that request.
- A module-qualified call such as
mod.foo()no longer mis-binds to a same-spelled class that differs only in case (for example aModclass in scope) on the bytecode VM. Call-site dispatch and static-value access are now case-sensitive on both backends, matching the tree-walking evaluator. - Calling a method that does not exist on a native class instance (for example a
Response) now raises a clear "unknown method" error on the bytecode VM, matching the evaluator, instead of the misleading "module ... is not loaded". reflect.class(instance)now includes the class's own decorators on the bytecode VM when the class is declared in another module, matching the evaluator. Previously class-level decorators were dropped across a module boundary on the VM, so code reflecting over a class from a different module (the pattern frameworks use to read class annotations) saw none of them.
1.7.2
Indexed iteration
enumerate()(list method andcollections.enumerate) pairs each element with its index, so you can iterate with the index in hand:for i, v in xs.enumerate(). Lists previously had no indexed-iteration form (dicts already supportfor k, v in d).
Multiple return values
- A function can return several values with
return a, b, and the caller unpacks them withlet a, b = f()ora, b = f(). The swap idioma, b = b, aworks too. Values are carried as a list (so the return type is a list);let a, b = ...is shorthand for list destructuring.
const parameters
- A function parameter can be declared
const(func f(const list<int> xs)) to make it read-only: the argument is shallow-frozen on entry, so mutating it inside the function raisesImmutableErrorwhile the caller's value is left untouched. Documents and enforces that a function only reads an argument.
Deep copies
- New
clone.deep(value)returns a deep copy of any value - containers and class instances are cloned recursively, primitives pass through, and resource handles are left as-is. Self-referential lists and object cycles are handled. - Lists, dicts, and sets gain a
deepCopy()method, the deep counterpart of the shallowcopy().
Fixes
-
dict.copy()now preserves insertion order on both backends (the tree-walking path previously returned the entries in an arbitrary order). -
Concurrent field access on an object shared across async tasks no longer crashes the interpreter. A per-instance guard makes ordinary field reads and writes safe under parallelism; it is gated so sequential code keeps its previous speed. Logical correctness for shared mutable state still needs a lock or atomic (see the async chapter); whole-object reads such as serialising or reflecting over an object while another task mutates it remain a data race to synchronise.
-
Iterating a channel with
for x in cno longer mutates the shared channel, so producer tasks sending on other goroutines can no longer trigger a concurrent-access crash. Iteration now uses a per-consumer cursor, which also lets two consumers iterate the same channel without clobbering each other.
String ergonomics
New string methods:
capitalize()/title()- upper-case the first character, or title-case each word (the rest is lower-cased).removePrefix(p)/removeSuffix(s)- strip a fixed affix if present.lines()- split on line boundaries (LF and CRLF; no trailing empty).isBlank()- true when empty or only whitespace.equalsIgnoreCase(other)/containsIgnoreCase(sub)- case-insensitive comparison and substring test.
More collection operations
Seven new list operations, available both as methods (xs.flatMap(f)) and as
collections module functions (collections.flatMap(xs, f)):
flatMap(fn)- map each element to a list and concatenate.uniqueBy(fn)- remove duplicates compared by a key function.takeWhile(fn)/dropWhile(fn)- leading run by predicate, and the rest.windowed(size, step=1)- overlapping sliding windows (complementschunk).unzip()- inverse ofzip: a list of pairs becomes[firsts, seconds].scan(initial, fn)- running fold returning every intermediate accumulation.
Time ergonomics
time.humanize(ms)renders a millisecond duration as a compact string:45ms,1.5s,3m 4s,2h 5m,1d 1h(largest one or two units).- New
time.stopwatchmodule with a monotonicStopwatchclass (elapsed,elapsedFloat,lap,reset) for lap timing without juggling timestamps. Backed bytime.monotonic(), so it is immune to wall-clock jumps.
Datetime ergonomics
Instantgained direct part accessors so you no longer index a parts dict:year(),month(),day(),hour(),minute(),second(),weekday()(ISO 1=Monday .. 7=Sunday),dayOfYear(),isWeekend(), plusinZone(zone).Instantcomparisons and conversions:isBefore,isAfter,equals,sub(duration),toUnix,toUnixMillis,toUnixNanos,formatHTTP.Durationarithmetic and conversions:add,sub,abs,negate,inSeconds,inMillis,inNanos.Zone.offset()returns the current UTC offset (alongside the existingoffsetAt(instant)).formatandparseaccept friendlier layouts: strftime tokens (%Y-%m-%d) and preset names (iso,date,time,datetime,http), in addition to the existing Go reference-time layout.parse(text, layout)now takes an optional custom layout.
Native module functions are first-class values
A function from an imported native module can now be referenced as a value,
not just called - let f = math.abs; or xs.map(math.abs) after
import math. This completes the first-class-function story: builtin type
statics (string.compare) already worked bare, and native module functions
now work once their module is imported.
Grapheme clusters (user-perceived characters)
Strings gain graphemes(), graphemeLength(), and truncateGraphemes(n),
which work in Unicode grapheme clusters (UAX #29) rather than code points.
A combining sequence ("e\u{301}") or an emoji ZWJ sequence (a family emoji)
counts as one grapheme even though it is several code points, so these are
the right tools for display width, truncation, and cursor movement.
length() / chars() / codePoints() remain code-point based.
Clearer sorting and searching
Sort callbacks are now consistent and more flexible. xs.sort(cb) /
xs.sorted(cb) accept either a less-than predicate (a, b) -> bool or a
three-way comparator (a, b) -> int, so a comparator like string.compare
can be passed directly (names.sort(string.compare)). Previously only a bool
predicate worked and the docs wrongly described a -1/0/1 comparator.
xs.sortBy(selector) takes an optional descending flag, and a new
xs.binarySearchBy(selector, key) searches a list sorted by a key. Builtin
type statics (string.compare, string.fromCodePoint, bytes.fromString,
...) are now first-class values, so they can be passed straight to sort, map,
and other higher-order methods without a wrapper. The collections guide now
documents Python-style slicing, including the step form (xs[::2]) and
reverse (xs[::-1]).
Escape sequences are decoded inside interpolated strings
Escape sequences in a double-quoted string that also contains ${...}
interpolation are now decoded, the same as in a non-interpolated string.
Previously "line\n${x}" emitted a literal backslash-n instead of a
newline, and "\u{1F600} ${name}" left the \u{...} escape undecoded.
Relatedly, an invalid \u{...} escape (empty, out of range, or a
surrogate) is now a clear compile-time error instead of producing a
malformed string. See the string-escape reference in the syntax guide,
which now documents \u{HEX} for Unicode code points.
geblang install pkg@latest resolves to the highest semver tag
geblang install <git-url>@latest now queries the remote with
git ls-remote --tags --refs, picks the highest stable semver
tag (skipping pre-releases unless every tag is one), and clones
that tag. Bare-numeric tags like 1.2.3 are accepted alongside
the vX.Y.Z shape. Non-semver refs (dev, release-1, branch
names) are ignored during resolution. Re-running install with
@latest always re-resolves; pinned versions keep their existing
lock-skip behaviour. New dependency: golang.org/x/mod (BSD-3-
Clause); added to NOTICES.md.
1.7.1
HTTP TLS: client verification control and HTTPS servers
HTTP clients verify TLS certificates against the system trust store by
default. A new tls option on http.newClient controls this: verify
(set false to skip verification), caCerts (PEM certificate(s) to
trust), caCertsOnly (trust only those, ignoring system roots), and
clientCert / clientKey (PEM, for mutual TLS). HTTP servers now serve
HTTPS when http.serve / http.listen are given a tls block: either
{cert, key} (PEM) or {selfSigned: true} to generate an in-memory
certificate for local development (with optional explicit SANs). The new
http.serverCert(server) returns the served certificate as PEM so a
client can trust a self-signed server precisely.
Builtin type static methods no longer require an import
Static methods on a builtin type - bytes.fromString(...),
bytes.fromHex(...), string.fromCodePoint(...), string.compare(...),
and the like - now resolve without import bytes; / import string;,
matching how the rest of the toolchain already behaved. Previously the
tree-walking evaluator rejected these with unknown method Type.X
unless the type was imported first, while compiled programs accepted
them; the two backends now agree.
Type-conversion methods for codepoints and byte lists
New methods round out converting between strings, codepoints, and byte
lists. s.codePoints() returns a string's Unicode code points as a
list<int> (the list form of codePointAt, and the inverse of
string.fromCodePoints). b.toList() returns a bytes value's byte
values as a list<int>, and bytes.fromList(list<int>) builds bytes
from byte values (0-255, rejecting out-of-range elements). Note that
string.fromCodePoint / codePointAt already serve as chr / ord.
Web request handler runs app-level before-middleware ahead of routing
The built-in web request handler now runs every app-level
before-middleware once, against the original request, before route
matching. Previously the middleware ran inside the matching loop after
the path had already been read, so a middleware could not rewrite
request["path"] and have routes match the new value. Two consequences:
middleware that strips or rewrites the path (locale prefix, version
prefix, host rewrites) now influences which route matches, and
before-middleware fires even when no route ends up matching (404).
Path parameters are no longer present on the request dict at the time
middleware runs; reading parameters is a routing concern that belongs
in a route-bound layer.
Top-level redeclaration is now consistently rejected
Declaring the same top-level name twice - an import and a func,
two lets, an enum and a class, an interface and a func, and
so on - is a compile-time error on both backends. The evaluator already
rejected these; the bytecode compiler used to silently let the later
declaration shadow the earlier binding, so a program could run under
one backend and misbehave under the other. Three cases stay allowed and
behave identically on both backends: function overloads (two funcs
with the same name and different signatures), re-importing the same
module, and re-binding a name after del. Type aliases live in a
separate namespace and never collide with values. A name brought in by
from M import X; is immutable: it cannot be locally redeclared or
overloaded (import the module and use M.X, or alias with as). Use
import X as Y; when the local name is taken.
del operates on variables only
del now applies only to variable bindings and the instances they hold
(whose destructor still fires). Deleting a class, function, enum, or
interface declaration is a compile-time error on both backends.
Previously the evaluator removed such a binding while the bytecode
backend handled it inconsistently, so the two could disagree. Re-binding
a variable after del (del x; let x = ...;) is unchanged.
Subclass constructor across modules no longer crashes
A subclass whose name matches its parent's (class X extends mod.X)
and whose constructor explicitly forwards via parent(...) no longer
fails at construction with no matching overload for X on the
evaluator. The VM was already correct. The fix targets the overloaded-
function dispatch path: when an explicit parent(...) call resolves
to the parent's constructor, the auto-parent-chaining trigger now
checks the matched function's owner class before re-firing, so
dispatching the parent's same-named constructor no longer re-attempts
the chain with zero arguments.
1.7.0
Runtime faults are catchable on both backends
Implicit runtime faults - division by zero, index out of range,
key-not-found, conversion failures like "abc".toInt(), and null access
- are now catchable with
try/catchidentically on the tree-walking evaluator and the bytecode VM. Previously the bytecode VM (used bygeblang run/geblang build) let these escapetry/catchand terminate the program, while the evaluator caught them, so the same code behaved differently betweengeblang testand a built binary.
FatalError tier
A new FatalError class sits outside the Error hierarchy and is never
intercepted by try/catch - not even catch (any e). It always
unwinds to the top and terminates. Raise one with
throw FatalError("message") for unrecoverable conditions. Exceeding the
maximum call depth (stack overflow) now surfaces as a FatalError on
both backends.
time.monotonic
time.monotonic() returns monotonic milliseconds since process start
and never decreases. Use it for durations, timeouts, and TTLs:
time.now() / time.unix() read the wall clock, which can jump
backwards on clock correction.
Shell completion
geblang completion bash prints a bash completion script. Enable it
for the current shell with source <(geblang completion bash), or add
that line to ~/.bashrc to make it permanent. It completes subcommands
at the first position (so geblang li<tab> becomes geblang licenses)
and filenames afterwards.
Paged licenses output
geblang licenses now pages its output through $PAGER (falling back
to less -R, then more) when run in an interactive terminal. Output
is written plain when piped or redirected, so geblang licenses > file
and CI capture are unaffected. Pass --no-pager to force plain output
on a terminal.
Exact decimal formatting in f-string specs
Numeric format specs (:f, :e, :g, :%) on a decimal value now
format from its exact value instead of routing through a binary float,
so ${d:.Nf} matches d.toString(N) with no binary-rounding artifacts.
float values are unchanged.
Dual-name modules (native + stdlib)
The import resolver now lets a native module and a Geblang stdlib
.gb module share the same canonical name. From outside the
stdlib wins; from inside, self-import returns the native module so
the wrapper can call its primitives. A missed export on a module
receiver falls back to the native registry, so users can reach
both surfaces through a single alias.
import async.sync as sync;
let m = sync.Mutex(); # stdlib OO wrapper class
let h = sync.mutexNew(); # native handle, via the same alias
Used by the new async.sync and async.atomic modules (below).
Existing dual-named pairs that previously had to use distinct names
(strbuilder + strings.StringBuilder, etc.) keep working unchanged.
Channels (async.channel)
Typed message-passing between tasks. Channel<T>(buffer = 0)
creates a channel; buffer = 0 is synchronous handoff, positive
buffer queues up to N values before sends block.
import async;
import async.channel as ch;
let c = ch.Channel<int>(0);
async.run(func(): void {
for (let int i = 0; i < 5; i++) { c.send(i); }
c.close();
});
for (var v in c) {
io.println(v);
}
Methods: send, recv, tryRecv, trySend, close, isClosed.
recv() returns null once the channel is closed and drained, so
for (x in channel) iterates naturally to the end.
Send-after-close and double-close throw. Recv on a still-open empty
channel blocks; tryRecv returns null without blocking when
nothing is pending.
select statement
select waits on multiple channel operations and runs the case
whose op fires first. New select keyword in the lexer.
select {
case let v = c1.recv(): handleA(v);
case c2.send(x): handleB();
default: nothingReady();
}
Case heads are c.recv() (with or without a let binding) or
c.send(value). default makes the select opportunistic; without
it the select blocks. When several cases are simultaneously ready
the chosen one is pseudo-random so producers and consumers cannot
starve each other through ordering. Backed by Go's reflect.Select.
Synchronisation primitives (async.sync, async.atomic)
Two new sub-modules under async add the canonical concurrency
building blocks. async.run already spawns real goroutines, so
these primitives coordinate across them.
import async;
import async.sync as sync;
import async.atomic as atomic;
let counter = atomic.AtomicInt(0);
let wg = sync.WaitGroup();
for (let int i = 0; i < 100; i++) {
wg.add(1);
async.run(func(): void {
counter.add(1);
wg.done();
});
}
wg.wait();
io.println(counter.load()); # 100
async.sync exposes Mutex, RWMutex, Semaphore, and
WaitGroup. Each constructor returns an instance whose methods
delegate to Go's sync package; Mutex.tryLock,
RWMutex.tryLock / tryRLock, and Semaphore.tryAcquire
provide non-blocking variants.
async.atomic exposes lock-free AtomicInt (int64) and
AtomicBool. Operations are sequentially consistent;
compareAndSwap(old, new) returns whether the swap happened.
1.6.0
geblang check: clearer error-versus-warning contract
geblang check now follows one contract: an error is code both execution
backends reject, and a warning is advisory and never changes whether code
runs. Code that the tree-walking evaluator runs but the bytecode VM cannot
build yet is reported as a vm-unsupported warning instead of an error, so
geblang check agrees with geblang test while still flagging what would
need --disable-vm for geblang run / geblang build.
profiler available on the evaluator
The profiler module (snapshot, delta, memory, cpu, peak) now
works on the evaluator - and therefore in geblang test - in addition
to compiled runs, so profiling helpers behave identically on both
execution paths.
List, set, and dict comprehensions
New Python-style comprehension syntax for building a list, set, or dict
from an iterable in one expression. Multiple for clauses nest;
multiple if filters chain as logical AND.
let evens = [x for x in xs if x % 2 == 0];
let squares = {x * x for x in xs};
let byId = {u.id: u for u in users};
let pairs = [a + ":" + b for a in xs for b in ys if a != b];
The binder accepts the same forms as the for-in loop: untyped,
typed (for int x in xs), or destructuring (for k, v in d.items()).
The lazy generator-comprehension form (expr for x in xs) is not
included in this release.
Pipe operator |>
Elixir/F#-style pipe injects the left value as the first positional argument of the right-hand call:
xs |> filter(positive) |> map(double) |> sum()
# = sum(map(filter(xs, positive), double))
The right-hand side can be a call (x |> f(a) -> f(x, a)), a bare
identifier (x |> f -> f(x)), or a selector (x |> mod.fn(a) ->
mod.fn(x, a)). The operator is left-associative and binds at very
low precedence so each side absorbs full expressions.
Spread in list / dict / set literals
...source is now a valid entry inside a list, dict, or set literal,
splicing the source's elements into the new collection.
[0, ...xs, 4] # list spread
{...defaults, "port": 443} # dict spread - last-write-wins on key collision
{0, ...someSet, 4} # set spread - sources can be set or list
{...a, ...b} # all-spread literals default to dict merge
List spread requires a list source; dict spread requires a dict source; set spread accepts a set or a list. A literal whose entries are all spreads is treated as a dict by default; force a set form by including at least one bare element.
Or-patterns in match
case A | B | C => ... matches when any alternate matches. Alternates
are bindless and cover three pattern kinds: literals (case 1 | 2),
bare types using Geblang's existing union-type syntax (case int | float), and enum variants without payload (case Color.Red | Color.Blue).
match (v) {
case int | float | decimal => "numeric";
case 1 | 2 | 3 => "low";
case Color.Red | Color.Blue => "warm";
default => "other";
}
Guards apply to the whole or-pattern. Bindings inside alternates are not supported in this release.
This change also fixes a pre-existing bug where union-typed
case T | U => patterns matched only the first arm; the dispatcher
now consults the full type string on both backends.
f-string format specifiers
String interpolation now accepts a Python-style format spec after the
expression: ${expr:spec}. The spec follows
[[fill]align][sign][#][0][width][,][.precision][type] with type
characters d, x, X, o, b, f, e, g, s, and %.
"${pi:.2f}" // 3.14
"${1234567:,}" // 1,234,567
"${42:>5}" // " 42" (right-align width 5)
"${42:05}" // 00042 (zero-pad)
"${255:#x}" // 0xff
"${0.125:.2%}" // 12.50%
"${name:.3}" // first 3 chars
The f / e / g types operate on decimal as well as float,
matching Geblang's default-decimal numeric convention. Width and
alignment also apply to strings. Plain ${expr} (no :) behaves
exactly as before.
Math constants
Twelve new zero-arg constant functions on the math module, matching
the existing math.pi() / math.e() shape:
| Constant | Value |
|---|---|
math.tau() |
2 * pi |
math.ln2() |
natural log of 2 |
math.ln10() |
natural log of 10 |
math.sqrt2() |
square root of 2 |
math.phi() |
golden ratio |
math.sqrt2Pi() |
sqrt(2 * pi) |
math.log2Pi() |
log(2 * pi) |
math.maxInt() / math.minInt() |
int64 limits |
math.maxFloat() / math.minFloat() |
float64 limits |
math.epsilon() |
smallest float eps such that 1 + eps != 1 |
Bug fix: try/catch across stdlib module boundary (VM)
Fixed a VM-mode regression where exceptions thrown from inside a
class method defined in an imported stdlib module were not caught
by a try / catch in the calling module. The dispatcher's
foreign-class native-trampoline branch was wrapping the inner
error with runtimeError, which collapsed the typed-throw chain
to a plain string before the calling VM could propagate it to its
exception-handler stack. The evaluator path was always correct;
behaviour is now consistent across both backends.
import option;
try {
option.Option(false, 0).unwrap();
} catch (ValueError e) {
# now catchable on the VM, as on the evaluator
}
Bug fix: iterator dispatch across stdlib module boundary (VM)
Fixed a sibling VM-mode regression for the iterator protocol:
for (x in instance) failed with <Class> is not an iterator
when the instance's class was defined in an imported stdlib
module. The user-iterator dispatcher looked the class up via the
running chunk's local class table, which doesn't contain
foreign-module classes. Fix routes the __done / __next
presence check through the trampoline table the module loader
populates at import time, and threads any thrown errors back to
the calling VM's pendingThrow via the same propagation path the
catch fix uses.
import deque;
let d = deque.Deque<int>();
d.pushBack(1); d.pushBack(2);
for (var x in d) {
# now iterates on the VM, as on the evaluator
}
assert builtin
New top-level assert(cond) / assert(cond, message) builtin and a
companion AssertionError class (direct subclass of Error). When
cond is false the call throws AssertionError; otherwise it is a
no-op. With no explicit message, the error includes the source text
of the condition expression so failures are self-describing:
assert(balance >= amount, "insufficient funds");
assert(1 == 2);
# AssertionError: assertion failed: (1 == 2)
Both geblang <script> and geblang build accept a --no-assert
flag that elides every assert(...) call at compile time. Neither
the condition nor the message is evaluated when the flag is set, so
the call is truly zero-cost (caveat: side effects inside assert
arguments are lost). geblang test always runs assertions.
The LSP catalog also surfaces signatures and hover docs for
assert, typeof, range, dump, and dir, which until now
were callable but invisible to the IDE.
Cron expression parser (cron)
New native cron module: parses standard 5-field cron specs
(plus @hourly / @daily / @weekly / @monthly / @yearly / @annually / @midnight shortcuts) and computes their next
firings. Hand-rolled, no Go dependency.
import cron;
import time;
if (cron.isValid(spec)) {
let next = cron.nextAfter(spec, time.unix());
}
let preview = cron.nextN("0 9 * * 1-5", time.unix(), 5);
Surface: parse (returns a normalised dict with field arrays),
isValid (cheap bool), nextAfter (next firing strictly after a
unix-seconds time), nextN (next N firings). Standard Vixie
semantics: when both day-of-month and day-of-week are restricted,
they are OR'd. @reboot is intentionally rejected (it has no
scheduled firing). Field names (jan-dec, sun-sat) are accepted
case-insensitively.
IP / CIDR utilities (net)
The net module gains pure helpers for IP addresses and CIDR
ranges. Useful for allow-lists, deny-lists, classification, and
binary protocols. Backed by Go's net/netip.
import net;
io.println(net.cidrContains("10.0.0.0/8", "10.5.5.5")); # true
let c = net.parseCidr("192.168.1.0/24");
io.println(c["first"]); # 192.168.1.0
io.println(c["last"]); # 192.168.1.255
io.println(c["count"]); # 256
Surface: parseIp, parseCidr (returning a dict with network,
prefixLen, version, first, last, count), cidrContains,
cidrRange, isIpv4, isIpv6 (never throw), ipToBytes,
ipFromBytes. IPv6 CIDR counts lift to bigint automatically.
Unicode normalisation (unicode)
New native unicode module exposing the four Unicode
normalisation forms via unicode.normalize(s, form) and a cheap
unicode.isNormalized(s, form) check. form is the canonical
"NFC" / "NFD" / "NFKC" / "NFKD".
import unicode;
let canonical = unicode.normalize(userInput, "NFC");
if (!unicode.isNormalized(stored, "NFC")) {
log.warn("stored value is not NFC-normalised");
}
Backed by golang.org/x/text/unicode/norm. NFC composes, NFD
decomposes, NFKC / NFKD additionally fold compatibility
equivalents (ligatures, full-width, superscripts).
MessagePack codec (msgpack)
New native msgpack module with encode, decode, tryDecode,
and validate. Hand-rolled implementation - no Go dependency -
covering the MessagePack 5 common cases: nil, bool, signed
integers (int family), float64, str family, bin family, array
family, and map family.
import msgpack;
let bytes = msgpack.encode({"items": [1, 2, 3], "ok": true});
let value = msgpack.decode(bytes);
Type mapping is one-to-one for primitives and containers. bytes
round-trip via the bin family; decimal round-trips as a
MessagePack string (lossless, portable). Ext types and the
timestamp extension are not supported in 1.6.0; integers outside
int64 range raise on encode.
lrucache.LruCache<K, V>
New stdlib LRU cache with O(1) get / put / evict and optional time-to-live. Backed by a doubly-linked list (for ordering) plus a dict (for lookup); pure Geblang.
import lrucache;
let c = lrucache.LruCache<string, int>(100);
c.put("a", 1); c.put("b", 2);
io.println(c.get("a")); # 1 - now most recent
let withTtl = lrucache.LruCache<string, int>(100, 60); # 60s expiry
get(key) returns null on a miss (or on a hit whose entry has
expired). Pair with has(key) when you need to distinguish a
stored-null value from an absent key. Operations: get, put,
delete, has, length, capacity, isEmpty, clear,
keys, values, stats. stats() returns lifetime
{hits, misses, evictions, expirations} counters useful for
tuning capacity.
Expiry is lazy: an expired entry is dropped on the next get or
has, no background scan. Capacity must be at least 1.
deque.Deque<T>
New stdlib double-ended queue with amortised O(1) push / pop at both ends. Backed by a ring buffer that doubles in capacity when full.
import deque;
let d = deque.Deque<int>();
d.pushBack(1); d.pushBack(2); d.pushBack(3);
d.pushFront(0);
io.println(d.popFront()); # 0
io.println(d.popBack()); # 3
Operations: pushFront, pushBack, popFront, popBack,
peekFront, peekBack, get(i) (O(1) random access; negative
counts from the back), length, isEmpty, clear, toList.
Implements the iterator protocol so for (x in d) walks
front-to-back. popFront / popBack / peekFront /
peekBack / get throw ValueError on out-of-range access.
priorityq.PriorityQueue<T>
New stdlib priority queue (binary min-heap). Without a comparator,
elements are ordered by Geblang's < operator (works for int,
float, decimal, string); a func(T, T): int comparator
covers custom types or reverse order.
import priorityq;
let q = priorityq.PriorityQueue<int>();
q.push(3); q.push(1); q.push(2);
q.pop(); # 1
let byPriority = priorityq.PriorityQueue<Job>(
func(Job a, Job b): int { return a.priority - b.priority; }
);
Operations: push, pop, peek, length, isEmpty,
pushPop (atomic push-then-pop, useful for top-K), drain
(returns the remaining elements as a sorted list), and clear.
pop() and peek() throw ValueError on an empty queue.
Provably-fair RNG (secureRandom)
New secureRandom stdlib module for auditable random outcomes
(gaming, lotteries, public draws, anywhere "did the operator
cheat?" matters). It implements a commit / reveal scheme: the
server publishes the SHA-256 commitment of a freshly generated
32-byte seed, draws values from an HMAC-SHA-256 stream keyed by
that seed and the caller's clientSeed, then reveals the seed so
any third party can re-derive every draw and verify the
commitment.
let s = secureRandom.openSession({"clientSeed": "player#42"});
publish(secureRandom.commitment(s));
let roll = secureRandom.uintRange(s, 1, 7);
let seed = secureRandom.reveal(s);
audit(secureRandom.auditLogJson(s));
Draw helpers: bytes, uintRange, float, bool, choice,
shuffle, weightedChoice. Verification helpers:
verifyCommitment and replay (reproduces a single draw
outside any session). uintRange uses rejection sampling so the
distribution is unbiased even for ranges that are not powers of
two. After reveal the session refuses further draws.
For plain unpredictable randomness (session IDs, OTPs, salts),
keep using secrets.*. secureRandom is for the narrower case
where the audit trail matters.
Numeric precision methods
decimal and float gain value-keeping rounding methods that return
the same type, unlike math.floor/round/ceil which return int. Each
takes an optional number of decimal places (default 0); round rounds
half away from zero.
io.println((2.567).round(2)); # 2.57
io.println((2.5).round()); # 3
io.println((2.9).floor()); # 2
io.println((2.999).truncate(2)); # 2.99
io.println((3.14159f).round(2)); # 3.14
toDecimal now accepts an optional precision, converting and rounding
to that many places in one step:
decimal pi4 = math.pi().toDecimal(4); # 3.1416
New numeric helpers: sign() returns -1, 0, or 1; clamp(lo, hi)
constrains a number to a range and returns the receiver's type; isEven()
and isOdd() test parity of an int.
io.println((-7).sign()); # -1
io.println((12).clamp(0, 10)); # 10
io.println((4).isEven()); # true
The conversion methods (toInt, toDecimal, toFloat, toBool) work
on every primitive; value as type remains the idiomatic cast, with the
methods offering chaining and finer control.
Cross-module symbol checking in geblang check
geblang check now resolves module.member and from module import name against the actual exported surface of each module, for both
built-in modules and your own modules across a multi-file project. An
unknown member is reported as an error, so typos and outdated API calls
are caught statically:
$ geblang check app.gb
app.gb:2:4: error[import]: io has no exported member foobar
Checks resolve relative to each file and respect local scope, so a local variable that shadows a module name is not mistaken for the module. The same resolution backs the editor language server.
It also flags a method call on a typed instance whose class - across its parent chain and implemented interfaces, including classes imported from other modules - has no such method:
$ geblang check app.gb
app.gb:6:3: error[semantic]: Circle has no method bogus
The method check is conservative: it stays silent when the receiver's
type is not a statically known class, when the class or an ancestor
defines __call, when decorators may inject members, or when any part
of the hierarchy cannot be resolved.
Typos on built-in type methods (e.g. "x".fooBar(), (42).nope()) are
flagged too, checked against the authoritative per-type method set, and
a call to an undefined function (not a function, imported name,
constructor, variable, or built-in) is reported as well.
dir() reports the correct method set
dir(value) previously listed several string methods that do not exist
(trimLeft, padLeft, codeAt) and omitted many real ones. It now
reports the accurate, complete method set for each built-in type
(identical on both backends), including methods such as string.count
/ slice / reverse, the list collection helpers (groupBy,
chunk, zip, partition, topK, ...), and the dict graph helpers
(bfs, dfs, shortestPath, topologicalSort).
1.5.4
Bytecode VM: fused mod-zero branch
if (local % const_int == 0) and if (local % const_int != 0) now
compile to a single OpJumpIfModNotZero / OpJumpIfModZero
superinstruction on the VM. The opcode reads the int local
directly, computes the modulo against the constant divisor, and
branches in one dispatch, replacing the previous five-opcode
GetLocal+Const+ModInt+Const+JumpIfX sequence. The fast path
preserves Geblang's modulo semantics (negative-operand
correction, zero-divisor error). On the numeric_loop benchmark
this drops VM time by ~23% (95ms -> 73ms median).
json.stringify: skip the sort when keys are already ordered
json.stringify(dict) now iterates the dict's insertion-order
record (Dict.Order) when valid and tracks whether successive
keys are non-decreasing. When the dict's keys are already in
alphabetical order (the common case for parsed JSON being
re-stringified), the encoder skips the per-dict sort entirely.
Output ordering is unchanged: dicts built in non-alphabetical
insertion order still produce alphabetical output via the
fallback sort path.
json.parse: pre-sized Dict allocation
The parser now allocates each Dict with a capacity hint, avoiding
1-3 map and slice grow cycles per dict. Combined with the
stringify fast path the json_roundtrip benchmark drops by ~12%
(599ms -> 526ms median).
JSON encoder: zero-alloc direct dict encode + int formatting
json.stringify now writes dict entries directly while iterating
the dict's insertion-order record on the alphabetical fast path,
skipping the pooled pairs scratch slice and the sort entirely.
Integer formatting also uses strconv.AppendInt against a stack
scratch buffer rather than strconv.FormatInt, eliminating the
per-int string allocation and the corresponding GC pressure.
JSON parser: cached small-int interface wrappers
The parser now returns pre-boxed runtime.Value wrappers for
integers in [-128, 1024) from a process-wide cache, skipping
the per-call interface allocation that Go performs when wrapping
a struct-typed SmallInt. Cached values share identity but
compare and behave identically to freshly boxed ints. Combined
with the encoder changes, json_roundtrip drops by a further
~8% (526ms -> 498ms median).
Node.js added to the benchmark suite
benchmarks/run.sh now compares Geblang against Node.js alongside
CPython and PHP. Each of the nine benchmark workloads has a
benchmarks/node/<case>.js variant matching the existing
Python/PHP semantics. Host mode picks up node from PATH; Docker
mode pulls node:22-alpine by default, overridable via
BENCH_NODE_IMAGE. When a runtime is missing on the host the
corresponding rows are reported as skipped.
1.5.3
New copy-and-return list methods
list.reverse()returns a new list with elements in reverse order.list.reversed()is the alias.list.prepend(value)returns a new list withvalueat the front.list.unshift(value)is the alias.list.remove(value)returns a new list with the first occurrence ofvalueremoved; returns an equivalent list ifvalueis absent.
All three follow the existing copy-and-return convention used by
push, pop, sort, sorted and friends: a new list is allocated
and the receiver is unchanged.
Dict alias methods
dict.entries()is an alias fordict.items().dict.insert(key, value)is an alias fordict.set(key, value).dict.remove(key)is an alias fordict.delete(key).
dir() introspection fixes
dir(setValue)now returns the set's methods instead of an empty list.dir(dictValue)now returns the dict's methods instead of the dict's keys. The previous behaviour conflated data with surface.dir(listValue)now returns the full list-method surface, not the stale four-entry subset.dir(rangeValue),dir(stringValue), anddir(bytesValue)now use the canonical primitive-method tables, picking up methods added in previous releases.
Collections documentation
The collections reference (docs/user/stdlib/08-collections.md) gains
explicit coverage of the comparator shape for sort/sorted, the
canonical reverse-sort idiom, and tables for the new list and dict
aliases.
Semantic check rejects unknown lower-case type names
A typed declaration whose type name is fully lower-case and is neither
a built-in (string, int, ...) nor a declared alias, class, or
interface now errors at semantic-analysis time. This catches the
common typo aaa bbb; where two bare identifiers parse as a typed
declaration with aaa as the type. Generic type parameters (T,
U, ...) and PascalCase user types are unaffected.
REPL del and identifier lookup see prior-prompt bindings
The REPL now seeds each prompt's semantic analyzer with the names
already declared in the session. del x; (and any other identifier
reference) on a later prompt resolves to the binding from an earlier
prompt instead of failing with "unknown identifier".
Dict spread tolerates extra keys
foo(...dict) now silently drops dict keys that do not name a
parameter of foo, so options-dict patterns can carry more entries
than the target function consumes. Required parameters that the
dict does not cover still error; explicit foo(typo: 9) still
errors so typos are caught. Overload resolution prefers the
overload that drops the fewest spread keys when more than one binds.
The named-arguments and spread reference in
docs/user/05-functions-callables.md was rewritten to cover
positional/named mixing, ordering rules, dict spread semantics,
and overload interaction.
1.5.2
Lists are reference-typed; in-place growth methods landed
Lists now have full reference semantics: two variables bound to the
same list share its identity, and in-place mutations are visible
through every reference. This matches the semantics that index
assignment (xs[0] = v) already had and that other engines provide
for arrays / lists.
Three new in-place methods take advantage of this:
list.append(value)adds a single value to the end. Amortised O(1) per call; building a list of n elements is O(n) total work rather than O(n^2) as it was when accumulating withpush.list.extend(other)appends every element of another list.list.clear()empties the list.
All three return null, mutate the receiver, and propagate to every
alias. On a frozen list each one raises ImmutableError. When the
receiver still carries its declared element-type tag at runtime,
append and extend reject mismatched values with TypeError.
The previously copy-and-return methods (push, pop, prepend,
unshift, insert) continue to behave the same way: they allocate
a new list and leave the receiver unchanged. Reach for append when
you mean to grow the list; reach for push when you want a fresh
list back.
dict.clear()
New in-place method that empties a dict. dict.delete(key) already
mutated in place; clear rounds out the surface. Both raise
ImmutableError on a frozen dict.
freeze.shallow now freezes the receiver
Previously freeze.shallow(xs) for a list / dict / set returned a
frozen copy and left the original mutable. The behaviour now matches
the existing *Instance case and the documented "shares internal
data" promise: a single shared underlying value is marked frozen and
mutations through any reference are blocked.
If your code relied on the old behaviour to keep a mutable handle
alongside a frozen copy, build the copy explicitly: let frozen = freeze.shallow(xs.slice(0));.
1.5.1
Bytes
bytes.slice(start[, end])cuts a fresh bytes value out of an existing one. Negative indices count from the end; out-of-range bounds clamp. The two-arg form is half-open[start, end).
instanceof over generic collections
list<any>/dict<K, any>/dict<any, V>/set<any>are now universal-accept: every list / dict / set satisfies them, matching the documented "any accepts anything" rule.- Union arguments (e.g.
list<string|int>) match elementwise on untagged collections (each element must satisfy any arm) and satisfy the tagged-collection invariance check when the tag's type appears in the union (list<int>satisfieslist<int|string>). - Both fixes apply on the evaluator and bytecode VM in lockstep.
Methods on int work on every int representation
Chained calls like s.length().toString() no longer fail on the
evaluator backend with unknown method int.toString. Every
documented int method (toString, abs, isZero,
isPositive, isNegative) now dispatches on both runtime
representations.
Default arguments work in return position
A function declared with a default argument can now be called
without that argument from any call site, including return of
a function whose return type matches. The previous behaviour
required the caller to pass the explicit value (or bind the
result through let first) when the call appeared in return
position.
Dict insertion order is preserved
dict.keys(),dict.values(),dict.items(),for ... in dict, and string interpolation of a dict now return entries in the order they were inserted, deterministically. Updating an existing key keeps its original position; deleting and re-inserting moves the key to the end.yaml.parseandjson.parsepreserve the source mapping / object order.- Inspect / string-interpolation output of a dict no longer sorts keys alphabetically. The new order is "what you wrote".
1.5.0
Decorators
@abstractclass decorator: direct instantiation throws RuntimeError. Subclasses without@abstractinstantiate normally.@abstractmethod decorator: a class that declares (or inherits) any abstract method without a concrete override is itself abstract. Error message names the unimplemented method.- Class decorators that return a callable are now supported as the third runtime shape, alongside the existing register-in-place and swap shapes. The returned callable becomes the new constructor; the captured class value is marked raw so calling it from the closure builds the original without re-triggering the decorator chain.
- Typed delegation: a wrap closure may return an instance of a
different class than the decorated one. The runtime stamps the
instance so
instanceofagainst the original class still returns true, even though the runtime class is the replacement. Useful when one declared type fronts an implementation chosen by a decorator at definition time.
Field decorators run as write barriers
A field decorator whose name resolves to a callable in scope now runs on every assignment to that field (including the constructor's first write), transforms the incoming value, and the transformed value is what gets stored. Decorators stack bottom-up, output of one feeds the next. Names that don't resolve stay as pure metadata (the existing framework-annotation contract).
func upper(string v): string { return v.upper(); }
func minLen(int min, string v): string {
if (v.length() < min) { throw RuntimeError("too short"); }
return v;
}
class User {
@minLen(2) @upper
string name;
func User(string n) { this.name = n; }
}
Interface default methods and properties
Interface bodies now accept three forms:
- abstract method signatures (
func foo(): T;) - the prior surface - default method bodies (
func foo(): T { ... }) - implementing classes inherit the body when they don't override - property declarations (
string name;) - implementing classes gain the field automatically, no redeclaration needed
When two implemented interfaces both provide a default for the same method signature and the class doesn't override, the compiler rejects the class with an error naming both source interfaces. The rule fires only on conflicting defaults; one default + one signature inherits unambiguously.
JSON-like container Inspect
io.println(dict), io.println(list), and io.println(set) now
produce JSON-like output (sorted dict keys, quoted strings inside
containers, depth guard for cycles). Top-level strings stay unquoted
so io.println("x") -> x is unchanged.
Cross-module throws no longer swallowed
A method inherited from a parent class in another module could throw
silently: the bytecode VM's cross-module dispatch fallback treated any
loader error as "method not found" and dropped real throw errors on
the floor. The dispatcher now distinguishes the two and propagates a
throw to the calling VM's nearest try / catch.
In-process FFI for C-ABI shared libraries
New ffi stdlib module loads shared libraries through dlopen and
calls into them with no IPC overhead. Sits alongside the existing
subprocess ext protocol; use FFI for
hot numeric kernels and library bindings (libtorch, libsqlite,
libcurl, libopencv), ext for sandboxed or polyglot extensions.
ffi.dlopen(path)returns aLibraryhandle.Library.symbol( name, [argTypes], retType)returns a Geblang callable bound to the native function; invoking it dispatches into C through a per-signature trampoline cached on the library.- Type table covers
INT8-INT64,UINT8-UINT64,FLOAT,DOUBLE,PTR,CSTRING,BYTES,VOID. CSTRING marshals both directions; BYTES is zero-copy in. - Memory helpers:
ffi.alloc,ffi.free,ffi.readBytes,ffi.writeBytes,ffi.readCString,ffi.cString,ffi.errno. - C struct layouts via
ffi.StructOf([[name, type], ...]).Struct.sizereports the byte size,Struct.alloc()allocates one instance,Struct.get(ptr, name)andStruct.set(ptr, name, value)read and write fields with standard C alignment. ffi.callback(fn, argTypes, retType)wraps a Geblang function in a C function pointer for libraries that drive their own loop (qsort comparators, libcurl multi-handle, audio callbacks). Signature types restricted to INT*, UINT*, PTR. Callbacks live for process lifetime.- Typed arrays:
ffi.sizeOf(type),ffi.writeArray(ptr, type, list),ffi.readArray(ptr, type, length)for passing homogeneous arrays of primitives by pointer + length. Element types: INT*, UINT*, FLOAT, DOUBLE, PTR. ffi.bytesView(ptr, length)is the zero-copy view counterpart toffi.readBytes. The returned bytes value aliases the C memory; callers guarantee the buffer outlives every use.geblang bind <manifest.yaml>generates a Geblang module wrapping a C-ABI shared library from a declarative manifest (library + constants + structs + function signatures). The output is a normal Geblang module;importit and call exported functions like any other code. Sugar over the rawlib.symbol(...)form, useful for libraries with more than a handful of functions.- Capability-gated, default-off. Projects opt in through a
permissions.ffiblock ingeblang.yaml; standalone scripts opt in via repeated--allow-ffi <path-or-glob>CLI flags (also accepted bygeblang test).PermissionErroris a new built-in error class, catchable from Geblang. - Recommended pattern: wrap C handles in a Geblang class with
__enter/__exitand lifecycle them withwithblocks so the release call fires automatically at scope exit. geblang doctorreports the active FFI policy and allow-list rules. LSP catalog covers theffimodule surface; VS Code shipsffidlopen,ffisymbol, andffihandlesnippets.
Dispatch backs onto purego (pure-Go reimplementation of
dlopen + dispatch); no cgo, no extra build dependency. Supported
platforms: Linux/macOS/Windows on x86_64 and arm64.
See Foreign Function Interface for the full reference. Real-library acceptance tests cover libm (sin, cos, sqrt, hypot), libc (getpid, strlen, malloc/free, memcmp, errno), and a complete SQLite open/exec/prepare/step/finalize/close walkthrough.
Test framework
- New
this.assertThrowsOf(callable, classOrName[, substring])narrows the existingassertThrowscontract to a specific exception class.classOrNameaccepts either a class value (for user-defined classes in scope as identifiers) or a class name as string (works for the built-in errors that aren't reified -RuntimeError,TypeError,ValueError,IOError,ParseError,MatchError,ImmutableError,PermissionError). The match walks the parent chain like a catch clause, so a subclass instance matches the parent class. Optional third argument is a substring that must appear in the error message. Failure messages name both expected and actual class.
Language
- Dunder method names normalised to prefix-only:
__enter,__exit,__serialize,__deserializeare now the canonical forms, matching the rest of the dunder surface (__get,__set,__call,__eq,__read,__write,__close,__iter, ...). The legacy prefix-and-suffix forms still work so existing tests and scripts keep running. - Parameter-level metadata decorators: any name attached to a
function or constructor parameter (
@SomeName(args)) surfaces throughreflect.parameters(fn)as adecoratorskey per parameter dict, mirroring the existing class- and method- decorator metadata. Pure metadata; the runtime never invokes them. Frameworks read the structure to drive dispatch. - Bytecode VM identifier dispatch is now case-sensitive at the
call site, matching the evaluator. A module that exports both
a
viewfunction and aViewclass now resolves each call correctly; previouslyview(args)could bind to the class constructor and surface as "no matching overload for View" at runtime.
Cross-module
- Interface default methods and property declarations
(introduced earlier in 1.5.0) now propagate across module
boundaries. A class can
implements donor.Greetableand inherit a defaultgreet()plus a declarednamefield. reflect.fieldsreturns full type info for class references passed across modules; the receiving module's parity tests now see declared types and nullability rather than the collapsedany/ non-nullable fallback.- Two-hop class extension:
class Leaf extends middle.MiddlewhereMiddle extends donor.Baseresolves inherited methods through both module hops. Inheritedthrowcalls propagate to the caller'stry / catchacross the same chain.
Reflection
reflect.classes()enumerates every class declared in the current program (user classes plus imports). Useful for framework discovery passes that scan for@OnMessage,@Job,@Scheduled, or other class-level decorators without forcing the user to register handlers explicitly.
Stdlib
math.isPrime(n)tests primality on arbitrary-precision integers. Backed by Baillie-PSW plus Miller-Rabin (20 rounds), so deterministic for inputs that fit in anint64and effectively certain for larger values. Returnsfalseforn < 2including negatives.
Errors
- "Unknown method" now raises a catchable
RuntimeErrorcarrying the receiver class and missing method name; atry / catch (RuntimeError e)block reaches it on both backends. Previously the bytecode VM dropped the message on the floor for cross-module dispatch.
Engine parity
- Evaluator widens
SmallInttodecimalandfloatonascasts. Method results likelist.length()produce the compactSmallInt; the bytecode VM handled the widening but the evaluator only matched the big-integer variant, surfacing as "cannot cast int to decimal" ingeblang testruns where the same code worked undergeblang run.
Stdlib (messaging)
- AWS SNS backend for
messaging.topic({"driver": "sns", ...}).publish()signs each request with sigv4 and POSTs to the regional SNS endpoint;subscribe(handler)polls a paired SQS queue and forwards notifications to the callback. Joins the existingrabbitmq/stomp/kafkapub/sub drivers.
Stdlib (LLM)
- New
llmmodule: a provider-agnostic client for chat completions, text embeddings, image analysis, and image generation. Pick the backend withllm.client({"provider": "openai" | "anthropic" | "bedrock", ...}); the rest of the calling code is the same across providers. OpenAI covers all four operations; Anthropic covers chat + image analysis; Bedrock covers chat + image analysis through the Anthropic Messages schema, embeddings viaamazon.titan-embed-*andcohere.embed-*model families, and image generation viaamazon.titan-image-*andstability.*model families. Calls with an unsupported operation / unrecognised model family raise aRuntimeErrornaming the missing method and, for Bedrock, pointing at the lower-levelinvoke(model, payload)escape hatch.
Other
- Thread-safe WebSocket writes: concurrent sends from multiple
Geblang tasks on the same
WebSocketvalue no longer race the underlying TLS / TCP write path.
1.4.5
Engine
- Bytecode VM now supports
class Sub extends mod.Parentpatterns where the parent class lives in another.gbmodule. Bothparent(args)constructor calls andparent.method(args)dispatch through the parent module's chunk. Method lookup on the subclass instance also walks across the module boundary so inherited methods likesubInstance.parentMethod()work natively under the VM (the evaluator already supported this). Removes the long-standing requirement to run apps that import libraries with subclassable base classes via--disable-vm.
1.4.4
Stdlib
crypt.md5/sha1/sha256/sha512/sha3_256/blake2b/crc32now accept eitherstringorbytesinput (previously string-only).crypt.hmacSha256andhmacSha256Bytesaccept string or bytes for both the key and the message. Existing string callers are unchanged.
1.4.3
Time
- New
time.unix()/time.unixMilli()/time.unixMicro()/time.unixNano()/time.unixFloat()/time.unixDecimal()for PHP / Python-style unix-time access.time.unix()is whole seconds (PHPtime());time.unixFloat()is fractional seconds (PHPmicrotime(true)/ Pythontime.time());time.unixDecimal()is lossless nanosecond-precision seconds as adecimal. time.elapsedFloat(start)is the float-seconds analogue oftime.elapsed.time.now()keeps returning milliseconds; nothing inasync.sleep/ scheduler /timeoutMssemantics changes.
Networking
http.listen,http.serve, andnet.serveaccept an optionaloptsdict withmaxConcurrent,queueSize, andonOverload("reject" / "wait" / "drop") for bounded concurrency and backpressure. Defaults are unchanged - no opts means unbounded. WebSocket connections share the parent HTTP server's cap, so amaxConcurrent: 1000listen becomes a hard cap on simultaneous WebSocket clients.http.serverStats(server)andnet.serverStats(handle)return{active, queued, rejected, maxConcurrent}so callers can wire pool counters into metrics or alerts.
1.4.2
Language
- Selective imports:
from X import Y;,from X import Y, Z;,from X import Y as Z;. Binds the named symbols into the current scope without the module namespace prefix. The source module itself is not bound by the from-import - pair withimport X;when you need both.fromis a soft keyword so existing identifiers namedfrom(function parameters, class fields) still parse.
Bug fixes
- REPL: left / right arrows now follow the line correctly when the input wraps to a second terminal row, and Home / End jump to the start / end of the logical line instead of the start / end of the current physical row. Backspace, Delete, and history navigation also reposition the cursor properly across wrapped rows.
- REPL: pressing Enter after navigating away from the end of a
wrapped line now puts evaluator output on a clean new line below
the entire input, instead of overwriting the trailing wrapped row.
Tab-completion candidate listings and the
^Cclear-line message walk past wrapped rows the same way.
1.4.1
Stdlib
int.toString(base)andstring.toInt(base)accept any base 2-36 for arbitrary base conversion (lowercase digits a-z).encoding.base64UrlEncode/base64UrlDecodefor unpadded URL-safe Base64 (RFC 4648 section 5); decoder accepts padded or unpadded input.bytes.toBase64Url/bytes.fromBase64Urlmodule helpers and ab.toBase64Url()method on bytes values.crypt.passwordHash(pw, opts?)andcrypt.passwordVerify(pw, hash)produce and verify hashes interchangeably with PHP'spassword_hash/password_verify. Output uses the$2y$prefix for bcrypt (PHP default) or PHC format for argon2id / argon2i. Verify auto-detects the algorithm from the hash prefix and accepts$2a$,$2b$,$2y$,$argon2id$, and$argon2i$hashes from any compatible source.- New
binarymodule with Pythonstruct-style pack/unpack:binary.pack(format, ...values),binary.unpack(format, data),binary.unpackNamed(spec, data), andbinary.size(format). Format codes cover signed/unsigned 8/16/32/64-bit ints, 32/64-bit floats, fixed-length byte strings, and pad bytes; the first character may set endianness (<little,>big,!network,=native).
Tooling
- LSP diagnostics now cover unresolved imports, bytecode type errors,
unused imports, and cross-module symbol checks (
foo.bar()is flagged whenbarisn't exported byfoo). Bothgeblang checkand the in-editor squiggles go through the same shared pipeline. - New LSP capabilities:
textDocument/codeActionquick-fix for unresolved imports (suggests nearest-match replacements);textDocument/referencesandtextDocument/renamefor the identifier under the cursor (single-file scope);workspace/symbolsearch across every.gbfile in the open roots. - VS Code extension gains a
Geblang Language Serveroutput channel and a status-bar item showing the LSP state (click to focus the channel).editor.formatOnSaveworks as expected; no extension setting needed.
1.4.0
Performance
- Tight integer loops are noticeably faster:
BenchmarkIntLoop3.7 ms to 2.74 ms,BenchmarkIntArithmetic6.2 ms to 5.2 ms. recursive_fibscoreboard 86 ms to 67 ms.list_functionalscoreboard 14 ms to 12 ms (matches PHP).- Recursive call paths drop ~20 allocations per call from the new function-frame layout; long-running recursive workloads see lower GC pressure.
Language
- List-shape patterns in
match:case [int x, int y] if x > y => .... Each binding may be typed (must match) or untyped (any value);_is a wildcard that skips binding. Length and type mismatches both fall through to the next case. - Union types (
T | U) at parameter and return positions:func get(int | string id): User | NotFoundError. The runtime enforces "any branch matches" and throws a catchableRuntimeErroron mismatch (parameter-validation errors now go through the standard throw path on the VM, matching the evaluator). Intersection (T & U) supported with "every branch matches" semantics. - Structured concurrency via the
async.scopemodule:async.scope.TaskGroupwith.spawn(fn)/.cancel()and theasync.scope.scope(body)runner. The body receives the group; spawned children are awaited at scope exit; if the body or any child throws, remaining children are cancelled and the first error is rethrown after the drain completes.
Stdlib
- New
messagingmodule with a unified queue + topic facade and pluggable backends. Backends: AWS SQS over HTTPS with sigv4, RabbitMQ over AMQP 0.9.1, STOMP 1.2 (covers ActiveMQ natively and RabbitMQ via the STOMP plugin), and Kafka.messaging.connect({driver, ...})returns a queue handle withpublish/receive/ack/consume/close.messaging.topic({driver, ...})returns a pub/sub handle withpublish/subscribe/close; RabbitMQ uses a fanout exchange, STOMP uses/topic/destinations, Kafka uses a fresh consumer group per subscriber. SQS is queue-only on this surface; use AWS SNS for fan-out and target SQS queues as SNS subscriptions. Lower-levelamqpandkafkanative modules are exposed for cases beyond the facade. - New
archivemodule with zip / tar / tar.gz readers and writers:archive.zipRead/zipWrite/tarRead/tarWrite/tarGzRead/tarGzWrite. Entries are dicts withname,data(bytes),isDir, andsize; writers accept string or bytes payloads and sort tar entries by name for deterministic output. crypt.jwtSign(payload, key, opts?)andcrypt.jwtVerify(token, key, opts?)now dispatch on thealgoption (or the token header) and cover every supported algorithm in one pair: HS256 / HS384 / HS512, RS256 / RS384 / RS512, ES256 / ES384 / ES512, and EdDSA (Ed25519). Passopts.allowedAlgsto defend against alg-confusion attacks. The default allow-list excludesnoneon both sign and verify; opt in by passing"none"insideopts.allowedAlgswhen you genuinely need unsigned tokens.crypt.jwtSignRS256/jwtVerifyRS256/jwtSignES256/jwtVerifyES256remain as deprecated shims for 1.5.0 removal.- New
crypt.jweEncrypt(payload, key, opts)andcrypt.jweDecrypt(token, key)for encrypted JWTs. Key-wrap algorithms:dir(32-byte CEK) andRSA-OAEP-256(wraps a fresh CEK with an RSA public key). Content encryption isA256GCM. Tampered tokens fail the AEAD authentication and throw. - New
crypt.pkcs12Decode(pfx, password)returning{key, cert, caCerts}wherekeyis a PKCS#8 PEM and the certificate fields are CERTIFICATE PEM strings. Encoding to PFX is not in scope for 1.4.0. - New
crypt.signCertificate(options)signs a CSR with a CA certificate and key, returning the issued certificate PEM. Options:csr,caCert,caKey(all PEMs),validDays(default 365),isCA(default false),dnsNames,ipAddresses,serialBits(default 128). Completes the CSR-to-issued-cert pipeline (generateCsrtosignCertificatetoparseCert). metrics.counter(name, opts)/gauge/histogramdeclare typed metrics with optional labels;metrics.observe(name, value, labels)records histogram samples;metrics.toPrometheus()emits Prometheus v0.0.4 text exposition format. Legacymetrics.inc/setkeep working unchanged.log.toStream(stream)writes JSON log lines to anystreams.IOStream(memory buffer, TCP socket, pipe).trace.toOtlpJson(opts?)serialises recorded spans as OTLP/HTTP JSON;trace.exportOtlp(endpoint, opts?)POSTs them to a collector atendpoint/v1/traces. Child spans viatrace.start(name, attrs, {parent}).
Performance
- Faster tight loops that mix arithmetic with collection length /
modulo:
regex_match62 ms to 45 ms,numeric_loop123 ms to 92 ms,recursive_fib65 ms to 58 ms. BenchmarkRecursiveFiballocs/op 301 to 24.
Tooling
make benchnow builds geblang viamake buildfirst and benches that binary, so source changes always reach the scoreboard.
1.3.0
Stdlib
- New
pcremodule: PCRE-compatible regex engine for patterns that need lookahead, lookbehind, backreferences, atomic groups, or named captures via either(?P<name>...)or(?<name>...). Surface mirrorsre.*(test, find, findAll, match, matchAll, replace, split, quote) with an optional flags string (imsx). Coexists with the existingremodule (RE2, linear-time, no catastrophic backtracking) - reach forpcreonly when you need PCRE-only features.
Language
test.Test.assertThrows(callable, expectedSubstring = "")is now a built-in assertion on both the evaluator and the VM. Fails if the no-arg callable returns without raising; when the optional substring is given it must appear in the error message.test.run(class, opts)accepts a newmethodsoption (a list of method names) so tooling can run a single test.geblang test --class ClassNameand--method methodNameflags filter to a single class or method within the discovered test files.geblang test --format teamcityemits##teamcity[...]service messages (withlocationHint='geblang_test://Class/ method') so JetBrains IDE test runners parse events natively. Replaces the verbose PASS/FAIL output for IDE integration.test.mock(moduleName, {fname: callable})swaps stdlib functions for the duration of the current@testmethod; the runner snapshots patches before each method and restores them after, so mocks never leak across tests. Pair withtest.restore(module, fname)/test.restoreAll()for mid- method toggling.- New
crypt.hmacSha256Bytes(key, message): bytesreturns the raw HMAC output (instead of hex). Useful when the HMAC output is the next round's key - sigv4, HKDF, TLS PRF, etc. Verified against the AWS sigv4 reference vector.
Fixes
- VM: string
<,<=,>,>=now work the same way they did in the evaluator (lexicographic comparison). Previously the VM dispatched relational ops throughnative.NumericComparewhich rejected strings with "comparison expects compatible numeric operands"; the evaluator's owncompareValuesalready covered it. Both paths now share the extendedNumericCompare. New parity test guards the behaviour.
Tooling
- LSP:
this.<TAB>inside a class extendingtest.Testnow surfaces every inherited assertion (assertEquals, assertTrue, assertThrows, fail, etc.). - LSP:
<typedVar>.<TAB>and<typedVar>.<method>(<cursor>)on stdlib-class locals (http.Request, db.Connection, datetime.Instant, url.URL, streams.IOStream, proc.Process, sockets.Socket/Listener, ssh.SSHClient/SSHSession/SSHTunnel, strbuilder.StringBuilder, random.Generator, websocket.Connection, template.Template/Engine, log.Logger) surfaces methods with parameter and return types. Triggers when the local is declared via<module>.<Class> name;. - VS Code: new
assertThrowssnippet under the Geblang language.
1.2.0
Stdlib
- New
socketsmodule:sockets.dial(host, port, opts)opens a TCP or TLS connection and returns aSocketwrapping the stream protocol.sockets.serve(host, port, handler)binds a listener and dispatches each accepted connection to the handler callback.server.close()joins the accept goroutine so reads of module-level state from the parent happen-after the last handler invocation. Sockets implementread/readAll/readLine/lines/write/writeln/closeplus the dunder protocol forstreams.copyandfor (line in sock). - New
sshmodule: a Geblang-native SSH client.ssh.connect(target, opts)opens an authenticated connection (password / private key / passphrase / agent), with host-key verification viaknownHostsFile.client.exec(cmd)runs a one-shot command returning{stdout, stderr, exitCode}.client.spawn(cmd)returns anSSHSessionwithstreams.IOStream-shaped stdin / stdout / stderr (same shape asproc.Process), pluswait(),kill(),signal(name). SFTP:upload,download,sftpList,sftpRemove,sftpMkdir, andsftpOpen(returns an IOStream for piping remote files throughstreams.copy). Port forwarding viaforwardLocal(port, target)andforwardRemote(port, target)returnsSSHTunnelhandles that close cleanly. http.post/http.request/http.requestWithOptionsaccept astreams.IOStream(or any class wrapping one) for the request body, in addition to the existing string and bytes shorthand. Useful for multi-GB uploads that shouldn't load into memory.- New
cli.widgets.Spinnerandcli.widgets.ProgressBarrender ANSI control sequences to stderr (so stdout piping stays usable). The spinner hastick()/update(msg)/stop(); the bar hasadvance(n)/set(value)/updateLabel(label)/finish().
Bug fixes
- File / stream / socket close paths now suppress
already-closed errors so user code that closes the same handle
twice no longer surfaces the harmless errno (covers
os.ErrClosed,net.ErrClosed, and the "use of closed network connection" string fallback).
1.1.0
Stdlib
- New
streams.IOStreamclass wraps a file or in-memory handle withread,readAll,readLine,lines,write,writeln,flush,close, andfor (line in stream)iteration. Memory-backed instances also exposetoString(). - New factories:
streams.open(path, mode),streams.memory(initial), andstreams.stdin / stdout / stderr(). - New helpers
streams.readAll(src)andstreams.copy(src, dst)consume any value implementing the stream protocol. - New
procmodule starts child processes that stream concurrently with the parent:proc.spawn(cmd, args, opts)returns aProcesswithstdin,stdout,stderr(each anIOStream), pluswait(),kill(),signal(name), andpid.{pty: true}attaches a pseudo-terminal so interactive programs see a TTY. The existing synchronousprocess.run/sys.runare unchanged. - New
watch.start(path, callback, opts)registers an fsnotify watcher and firescallback({path, type})for each filesystem event.{recursive: true}walks subdirectories at register time.watch.stop(handle)waits for the in-flight callback to finish before returning. Polling helpers (watch.snapshot/watch.wait) remain available.
Language features
- Stream protocol dunders: classes implementing
__read(int),__write(string), and__close()plug intostreams.copy,streams.readAll, andfor (line in stream)directly. - Generator methods on user classes now run on the VM.
Bug fixes
io.readLineover an in-memory stream returnednullafter the first line; now reads each line in turn.- Cross-goroutine timer and ticker callbacks no longer trip the race detector.
- Methods on a main-script class invoked from a stdlib module
work without an
unknown classerror. sys.sleepandprocess.signalaccept any int value.
Performance
- Tight integer loops stay lock-free after the timer-race fix;
numeric_loop166 ms to 136 ms.
1.0.6
Performance
- JSON parse + stringify overhaul: shorter dict-key tags, repeated-
key interning, direct float/string encoders, pooled scratch
buffers;
json_roundtrip1665 to 520 ms, faster than Python's C json on the bench. - Function-call hot path passes function metadata by pointer rather
than copying a 350-byte struct on every call;
recursive_fib88 to 75 ms. - Method dispatch VMValue fast path on
instance.method(args);class_dispatch30 to 21 ms. - Tail-call elimination:
return f(args)reuses the current frame for primitive-typed-arg functions, removing the stack-depth ceiling on tail-recursive loops.
Language features
- Iterator protocol: classes with
__iter()/__done()/__next()work infor (x in obj). streams.Streamfluent collection pipeline (map,filter,take,toList,toSet,count,first,reduce,forEach,anyMatch,allMatch); lazy by default.reflect.location(target)returns{module, line, column}for functions, classes, closures, decorator targets, and instances.- Named arguments in
deferfor callable, instance-method, and module-function shapes. - Nested generic call-site inference:
list<dict<K, V>>and deeper shapes bind every leaf type parameter.
Bug fixes
- Cross-chunk closures invoked through a stdlib-module class method no longer resolve to the wrong function index.
Stdlib
- New
streamsmodule:streams.of(source)to wrap an iterable, plus theStreamclass.
1.0.5
Performance
acc = acc + "literal"in tight loops now uses a hidden builder;string_concat78 to 8 ms.- Callbacks (
collections.map/filter/reduceetc.) no longer rebuild a sub-VM per call;list_functional1604 to 13 ms. - Regex compile cache (
re.*);regex_match187 to 64 ms. - Field-access inline cache;
class_dispatch26 to 21 ms. - Tagged-
VMValuearithmetic on hot ops;dict_ops27 to 19 ms. - Compile-time folding of literal arithmetic; div-by-zero is a check-time error.
- Direct
runtime.Valueto JSON encoder.
Stdlib
strings.StringBuilderclass for explicit builder-backed string assembly.csv.parse/csv.parseDict/csv.stringifyfor in-memory CSV; options:delimiter,trimSpace.path.globnow supports Python-style**recursive matches.math.median/percentile/quantile/modeover numeric lists. R type-7 linear interpolation.- String methods
splitRegex,replaceRegex,matchesRegexfor regex-aware split / replace-all / test.
Benchmarks
Three new scoreboard benches: regex_match, json_roundtrip,
list_functional.
1.0.4
Bytecode VM hot-path performance + a lifted compiler parity gap, on top of the type-matcher fixes that surfaced building the Gebweb Tasks example app.
Language features
- Cast overloading via dunder methods. A class can now control
how its instances respond to
as TYPEcasts by defining__string,__int,__float,__bool,__decimal, or__bytes. The dunder's declared return type must match the target primitive; the semantic analyzer rejects mismatches at compile time, and the runtime double-checks the returned value. Falls back to the existing built-in cast logic when no dunder is defined for the target type. Parallel to the existing operator overloading dunders (__add,__lt, etc.). Newasync.token()builtin returns a fresh uncompleted Task used as a pure cancellation signal by the redesigned Timer/Ticker stdlib.
Known limitations
- Async callbacks that close over a
BytecodeClosure/Functionpassed to a stateful native module can race with parent VM state. The wrap layer that bridges Geblang callable values into native code captures the parent VM by pointer; when the native side then invokes the wrapped value on a goroutine (asasync.run,async.sleep, etc. do), reads ofvm.globalsfrom the worker goroutine race with continued writes on the main goroutine. The go test race detector flags two parity tests (TestParityTimerFires,TestParityTickerStops); functional output is correct on both backends but ordering of those reads is technically unsynchronised. Closing the race needs a refactor of the native bridge to thread a per-goroutine VM context; queued for a 1.0.5 architectural pass.
Performance
Profile-guided round (Geblang's own profiler.snapshot() /
profiler.delta() plus Go's pprof on recursive_fib,
string_concat, class_dispatch):
-
Static-type propagation through function calls and class fields.
staticIntExpr/staticStringExprnow recurse intoCallExpression(when every matching overload declares the target return type) andSelectorExpression(when the receiver isthisor a typed class instance and the named field is declared with the target type). Required flushingclass.FieldNames/class.FieldTypesintochunk.Classesbefore compiling each method body so field metadata is visible during method compilation. Profile showsvm.adddisappearing fromrecursive_fib(was 10.26% of CPU) andclass_dispatchhot paths; replaced by the inline-handledOpAddIntfamily. -
Fused string-append peephole.
local = local + "literal"andglobal = global + "literal"now compile to a singleOpAppendStringConst/OpAppendGlobalStringConstinstead of the three-opcode sequenceOpGetLocal/OpAddStringConst/OpSetLocal. Bytecode chunk format version bumped 53 to 54. -
REPL accepts
1 as bool. The REPL's auto-semicolon-insertion treated only identifier / numeric-literal stmt-end tokens as triggering insertion.boolis the only type name that lexes as a keyword (string/int/float/decimal/bytes lex asIdent), soas boolat the end of a REPL line gaveexpected ;, got EOF.Boolis now in the stmt-end set.
Bench impact (3-run medians, after the full sequence of Tier A + type-propagation + peephole + earlier 1.0.4 work):
| Bench | Pre-1.0.4 | After 1.0.4 | Δ |
|---|---|---|---|
| numeric_loop | 131 | 124 | -5% |
| recursive_fib | 92 | 86 | -7% |
| list_pipeline | 13 | 9 | -31% |
| string_concat | 84 | 70 | -17% |
| dict_ops | 24 | 19 | -21% |
| class_dispatch | 47 | 37 | -21% |
Geblang now beats Python on numeric_loop, list_pipeline, and
dict_ops; competitive on the rest. The remaining gap to PHP
(2.5x to 5.4x on the slow benches) is structural: Go interpreter
floor on dispatch, Go string-concat allocator pressure on
string_concat, and per-call frame setup on recursive_fib. The
roadmap entries for B1 (flat-stack locals) and C-tier interface
removal would address those; both queued for 1.0.5.
Tooling
profiler.delta/memory/peak/cpureturn dicts that are usable from Geblang. They previously returned*runtime.Dict(pointer); the VM's index handler only matchesruntime.Dictby value, sod["elapsed_ms"]raised "dict is not indexable". Now returnsruntime.Dictdirectly; the docs example actually works.
Documentation
docs/user/03-types.mdnow points tostdlib/08-collections.mdfor the full list/dict/set method catalogue (length,push,pop,slice,map,filter,reduce,keys,values,items,add,union,difference, etc.).docs/user/stdlib/08-collections.mdgains a "Keyed Functional Helpers" section enumerating the instance-method form ofsortBy,minBy/maxBy,topK/bottomK,sumBy,frequencies,indexBy,containsBy,binarySearch,lowerBound/upperBound,take,zipWith,lazyMap/lazyFilter, etc. (interchangeable with thecollections.X(list, ...)module form).
Earlier 1.0.4 performance work
-
OpAddstring fast-path (vm.go:add). The dispatcher used to callcallBinaryOperatorMethodfirst on every add, including the commonstring + stringcase where the built-instringtype has no__addmagic method. The runtime.String check now short-circuits at the top ofOpAddwhen both operands are strings; the method-dispatch detour is preserved for class instances on the left. -
Single-overload method dispatch shortcut (
vm.go:selectRuntimeFunction). Most user classes declare a single overload per method. The dispatcher now skips the matches-slice allocation + post-loop "ambiguous overload" check forlen(indices) == 1, going straight to arity + type validation on the lone candidate. Behaviour is unchanged. -
String-key dict fast path (
vm.go:dictKeyFor). A new helper inlinesruntime.Stringandruntime.SmallIntkey conversion (the 99% case) and falls through tonative.DictKeyfor composite keys. Wired into the hot dict ops: index get/set,dict.contains,dict.get, and thesetmembership check. -
Compile-time
OpAddStringopcode (compiler.go+vm.go). When the compiler can prove both operands of+are statically typedstring(viastaticStringExprmirroring the existingstaticIntExpr), it emits a specialisedOpAddStringopcode that runs the concat inline with no type switch or method-dispatch detour. Mirrors the existingOpAddIntfamily for ints. Bytecode chunk format version bumped 50 to 51. -
Method-pointer lookup cache (
vm.go:lookupMethodLower). A single-slot cache keyed by (class name, lowered method name) short-circuits theclassInfo.MethodsGo-map access on the second-and-later dispatches to the same method on the same class. Tight loops calling one method on one class (everyclass_dispatch-shaped workload) hit the cache on >99% of calls and skip the map lookup entirely.
Bench impact (median ms, before to after Tier 1 + Tier 2, over 3 runs):
| Bench | Before | After | Δ |
|---|---|---|---|
| numeric_loop | 131 | 129 | -2% |
| recursive_fib | 89 | 85 | -4% |
| list_pipeline | 13 | 9 | -31% |
| string_concat | 84 | 70 | -17% |
| dict_ops | 24 | 19 | -21% |
| class_dispatch | 47 | 38 | -19% |
Geblang is now faster than Python on numeric_loop,
list_pipeline, and dict_ops; competitive on the rest.
New parity tests TestParityStringAddFastPath,
TestParitySingleOverloadMethodDispatch,
TestParityDictKeyFastPath, TestParityOpAddStringStaticTyping,
TestParityMethodLookupCache; new language test
tests/core/vm_hot_path_test.gb.
Tier A follow-up (vm.go + bytecode.go):
-
OpAddStringwrites the result VMValue directly into the stack slot, mirroring theOpAddInt-family inline write. The handler reads operands fromvm.stack[n-2]/vm.stack[n-1]without callingvm.pop()/vm.push(), so the interface materialise + function call overhead drops out. The interface box for the resultruntime.Stringis unchanged and dominates the per-iteration cost; closing that gap is the job of B2 (VMKindString variant on VMValue), planned next. -
Per-call type-validation loop is now skipped for functions whose params are entirely empty /
any-typed. A newFunctionInfo.requiresParamValidationbool is precomputed at chunk-load time (inprepareFunctionTypeMetadata); both the fast and slow call paths short-circuit the whole validation walk when it's false. -
Inline-cache experiment on
OpMethodCall/OpMethodCallNameddid NOT ship. A per-instruction class-pointer cache was implemented, measured, and reverted: the existing VM-globalmethodLookupClass/Name/Indicessingle-slot cache already hits99% on the monomorphic call sites the
class_dispatchbench exercises, so the per-call bounds check and cache compares the inline cache added cost more than they saved. Documented as a future candidate when a polymorphic-call benchmark exists. -
VMKindStringvariant onVMValuedid NOT ship. A new kind was added with an inlineStr stringfield soOpAddStringcould skip the interface-box heap allocation per push. GrewVMValuefrom 32 B to 48 B (+50 %), which regressedstring_concat(69 to 82 ms),dict_ops(18 to 23 ms),class_dispatch(37 to 42 ms), andrecursive_fib(85 to 95 ms) via worse cache locality on the stack/locals/globals slices. Reverted. The runtime.String interface box remains the dominant per-iteration cost onstring_concat; closing it cleanly needs either a smarter VMValue layout (e.g. unsafe-overlay onto the existing Boxed field) or compile-time string interning that reduces the alloc rate enough to make the GC pressure go away.
Parity
-
Empty-container defaults compile to bytecode directly. Parameter and class-field defaults of the form
dict opts = {}andlist xs = []previously routed through the evaluator (compiler.go:4757rejected anything beyond primitive literals). The compiler now accepts emptyDictLiteral,ListLiteral, andSetLiteralas defaults; the runtime constant pool gets three new tags (10/11/12) for the empty containers. To avoid the Python-style mutable-default trap, the VM clones the container at fill time via a newcloneContainerDefaulthelper, so each call (or new class instance) sees a fresh empty container. Non-empty container defaults (e.g.list xs = [1, 2, 3]) still fall back to the evaluator - lifting those needs full expression evaluation at call time, which is a bigger restructuring of the calling convention. Bytecode chunk format version bumped 51 to 52. New parity testTestParityEmptyContainerDefaults; new language testtests/functions/empty_container_defaults_test.gb. -
static funcmethods compile to bytecode directly. Previously any class with astatic funcdeclaration tripped thecompiler.go:741"does not support static functions yet" parity error and the CLI fell back to the tree-walking evaluator. The parity error reflected an incomplete implementation, not a real constraint: the runtime infrastructure for static methods (class.StaticMethods,OpCallStaticMethod,OpGetStaticValue) was already in place. The compiler now lowers static method bodies through the same pipeline as regular methods (skipping the implicitthisreceiver), so scripts using static methods (including every@ApiResourceentity in Gebweb that carriesstatic func repositoryClass()) now run purely on the VM. New parity testTestParityStaticFunctionLifted; new language testtests/classes/static_methods_test.gb. Two pre-existing Go tests that asserted the static-func rejection were updated to use non-literal class field defaults as their canonical "still-unsupported" feature. -
Spread arguments on a callable VALUE compile to bytecode directly. Two compiler.go sites used to reject spread on callable values: parenthesized-selector callable expressions (
(obj.fn)(...args), line 2440) and complex callable expressions (fns[i](...args),getFn()(...args), line 2602). Both forms now emit the sameOpMethodCallSpreadwith the__invokemethod name that the existing identifier-callable spread path uses. Static args before the spread are supported ((h.adder)(1, ...rest)); named args mixed with spread are rejected at compile time as before. New parity testTestParityCallableSpread; new language testtests/functions/callable_spread_test.gb.
Bug fixes
-
cli.tableaccepts the documented options-dict form. The user manual (docs/user/stdlib/13-cli.md) showedcli.table(rows, {columns: [...], headers: [...], separator: " | "})but the implementation only accepted an optional bare list of header strings; calling with a dict raisedcli.table headers must be list<string>. The implementation now accepts both forms: the legacycli.table(rows, ["A", "B"])AND the documented options dict.columnspicks the dict fields to render and their order;headersoverrides the display labels (defaulting to the column key names);separatorcustomises the inter-column gap (defaulting to two spaces). The legacy list form continues to work unchanged. -
REPL multi-line container literals no longer get a spurious semicolon injected (
cmd/geblang/repl.go:replInsertSemicolons). The ASI-style semicolon-insertion walked the token stream looking for statement-ender tokens at line ends, but didn't track bracket nesting. A list of dict literals like:let rows = [ {"name": "Alice"}, {"name": "Bob"} ];had a
;inserted after the closing}of each inner dict (because}is a statement-ender token), splitting the outer list literal and producingexpected next token to be ], got ;. The injector now tracks(/[nesting depth across the source and only inserts at depth 0. Braces{are deliberately not counted so semicolons are still inserted inside function / if / for bodies. New regression testTestReplInsertSemicolonsRespectsNesting. -
User class named
Taskno longer collides with the runtime asyncTask. The evaluator's overload / parameter type-matcher unconditionally rejected any value flowing into aTask-typed parameter when the value wasn't a*runtime.Task. A user-declaredclass Task { ... }therefore broke at every dispatch.repo.save(t)withsave(Task entity)and a user-class Task argument errored asno matching overload. The matcher now falls through to user-class matching when the value isn't the async Task primitive, so a user class namedTaskworks exactly like any other user class. The VM was already correct (its type-name dispatch routes throughvmTypeKindForBaseand never hard-codes theTaskstring), so only the evaluator path needed the fix. -
?UserClassparameter matching on the VM. The VM'sparseVMTypeSpeckept the leading?onspec.base, so the user-class comparisonvalue.TypeName() == spec.basealways failed for nullable parameter types like?AuthConfigor?Task. Only the?Tshape for primitives was working (those routed through the kind switch). The parser now strips the?fromspec.baseat parse time and carries the nullability on the separatespec.nullableflag, so a?UserClassparameter accepts aUserClassinstance again.
New parity test TestParityUserClassNamedTaskNoCollision; new
language test tests/classes/user_class_named_task_test.gb.
1.0.3 (released)
Small parity / ergonomics fixes uncovered while extending Gebweb.
New
web.parseMultipart(request). New native that decodes amultipart/form-datarequest body into a{fields: dict<string, string>, files: dict<string, dict>}dict, where each file entry is{filename, contentType, bytes}. Returns an error (catchable, wrappable as 400) when the body isn't multipart or the boundary is missing. Gebweb'sdict<string, UploadedFile>parameter binding is built on top of this. New parity testTestParityWebParseMultipart.
Bug fixes
-
Import alias collisions no longer leak across files (evaluator). The evaluator kept a process-wide
importNamesmap that recorded the LASTimport X as Yper aliasY, so two files that both used the same alias for different canonical modules collided: e.g. a user fileimport web.websocket as websocket;overwrote the alias mapping that stdlibimport websocket;had registered for the native, and stdlib code callingwebsocket.upgrade(...)routed through the user's wrapped module instead, surfacing as "module websocket has no export upgrade". The fix adds aCanonicalfield toruntime.Module(per-binding canonical module name) and has the call dispatcher consult the env-local binding'sCanonicalbefore falling back to the shared map. The VM was already correct because each compiled chunk owns its own globals; only the evaluator needed the fix. New parity testTestParityImportAliasDoesNotCollideAcrossFiles; new language testtests/core/import_alias_collision_test.gb. -
list.sort()is an alias forlist.sorted(). The LSP catalog (catalog.go:142-143) advertised both names but onlysortedwas wired into the method dispatcher, so user code reading the documented surface and callingxs.sort()failed withlist has no method sorton both backends. Both names now route to the same implementation. New parity testTestParityListSortAliasesSorted; new test methodslistSortMethod/listSortWithComparatorintests/stdlib/collections_test.gb. -
geblang checkrecognises thestringnative module. The CLI'snativeImportModulesallowlist - used bygeblang check's import-resolution pass to skip native modules that have no on-disk source - was missingstring, so any file withimport string;producederror[import]: cannot resolve import stringeven though the evaluator and VM both registered the module. Tests passed because the test runner doesn't gate on check diagnostics;make check-langexposed the false-positive. Added"string"to the allowlist alongside"smtp".tests/core/cross_type_casts_test.gbis now check-clean. -
Cross-module
implementson the bytecode compiler. A class declaringimplements mod.Interface<T>against an interface exported from another module - the canonical case isclass WidgetRepo implements repository.Repository<Widget>in Gebweb - failed the bytecode compile withbytecode compiler interface mod.Interface is not declaredand fell back silently to the evaluator. The evaluator'sresolveTypeValuealready walks imports; the compiler'sc.interfaceslookup only knew about locally-declared interfaces, mirroring a parent-class case that was already allowed atcompiler.go:891. The compiler now accepts any dotted name (strings.Contains(name, ".")) under bothimplementsclauses and interfaceextendsclauses, and the VM'sinterfaceMatchesstrips the module prefix from the stored name soinstanceof Repositorycontinues to match the module-qualified entry stored on the class. Six gebweb test files- the
widgets.gbexample that previously errored ongeblang checknow compile cleanly to bytecode. New parity testTestParityCrossModuleImplements; new language testtests/classes/cross_module_interface_test.gb.
- the
-
funcvalue casts tocallableon the evaluator. The evaluator's CastExpression handler matched the value'sTypeName()("func") against the target ("callable") and rejected the cast withcannot cast func to callable. The VM accepted it because itscastValuereturnedvaluewhen the target was a known alias of the value's runtime type. Both backends now route thecallable/func/functionfamily throughruntime.IsCallableValue, so any callable runtime value (Function, OverloadedFunction, BytecodeFunction, instance with__invoke) casts cleanly. Surfaced from gebweb middleware helpers that store user callbacks in adict<string, any>options dict and later cast them back tocallable. New parity testTestParityFuncAsCallable; new test classFuncAsCallableTestintests/core/cross_type_casts_test.gb.
1.0.2 (2026-05)
A quality-of-life release polishing two papercuts that surfaced while building Gebweb.
Highlights
- Range-to-list shorthand. The top-level
range(start, end[, step])builtin returns alist<int>directly, inclusive of both endpoints.Rangegains a.toList()method for symmetry with the literal form(1..5).toList(). The char-range literal'a'..'z'now produces alist<string>of single-character entries eagerly, solet list<string> letters = 'a'..'e'works without an intermediate conversion.list.toList()is a no-op pass-through so the same.toList()call works whether you have aRange, aSet, or already a list. - Tagged generic collections.
list<T>,dict<K,V>, andset<T>values flowing through a typed declaration or parameter boundary now carry their declared element types as a reified tag.reflect.typeBindings(xs)on alist<int>returns{"T": "int"}; untagged collections (raw literals not bound to a typed name) return{}rather than erroring. The "not preserved at runtime" caveat in chapter 3 is gone. instanceof <TypeRef>. The right operand ofinstanceofis now a full type reference -xs instanceof list<int>,d instanceof dict<string, User>,x instanceof ?intall parse and dispatch. Tagged collections compare bindings invariantly (same rule as 1.0.1 user-class generics); untagged collections fall back to a structural walk over their elements.- Reflection harmonised across backends and primitives. The
reflect API now produces the same shape on the evaluator and the
VM, regardless of whether you pass a class value, a class instance,
or a name string.
reflect.class("Foo")resolves a class declared in another loaded module via the module loader.reflect.methods(value)accepts an instance or a primitive (reflect.methods([1, 2, 3])returns the list method names,reflect.methods("hi")returns the string method names).reflect.fields(class)now returns a list of{name, type, nullable, hasDefault}dicts (was a list of name strings) - the type info was previously discarded. Cross-moduleinstanceof Parent,e as Parent, andcatch (Parent e)all walk the full parent chain (error-derived classes capture their parents at construction so the chain is reachable even when the catch site is in a different chunk). json.parseAs(text, ClassWithoutCtor)data-class shape. Classes without a constructor now have their fields populated directly from the parsed dict (previously they were instantiated with empty fields). Matches the canonical "data class" usage.- Numeric
//andas intclose cross-type gaps. The floor- division operator//now acceptsdecimal // decimalandfloat // floatin addition toint // int. Result type matches the operands (same-kind policy):7 // 2is3,7.5 // 2.0is the decimal3.0000000000,5.5 // 2.0(float) is2.0. The companion%is a floor-modulo on all three numeric types, so the sign of the remainder follows the divisor (-7 % 2 == 1,-7.5 % 2.0 == "0.5000000000"). Theas intcast now truncates toward zero fromdecimalandfloat(e.g.2.7 as int == 2,-2.7 as int == -2) and acceptsbool(true as int == 1). Previouslydecimal as intrejected any non-integer-valued operand andbool as intwas not supported. New parity testsTestParityFloorDivOnDecimalAndFloatandTestParityCastTruncatesDecimalAndFloat; new language testtests/core/floor_div_and_cast_test.gb. - Cast failures are catchable on both backends. A failed
x as Y(e.g."hi" as bytes) used to escape the VM as an uncatchablebytecode runtime error: cannot cast ..., even inside a surroundingtry / catch (RuntimeError e). The evaluator already raised a catchableRuntimeError. The VM now throws the same catchableRuntimeErrorvia the typed- throw path, so frameworks and user code can defensivelytrya cast on both backends. New parity testTestParityCastErrorIsCatchable; new test casecastFailureIsCatchableintests/errors/try_catch_test.gb. getMessage()andgetClass()on built-in errors. Built- in error values previously only exposed the.messageand.classfields. The Java / PHP / Python idiom -e.getMessage(),e.getClass()- errored with "X has no method getMessage". Both accessors are now methods on everyError-derived class, including user-defined subclasses (they dispatch through the same built-in path). The fields still work; choose either form. New parity testTestParityErrorGetMessageAndGetClass; new test casesgetMessageAndGetClassOnBuiltinandgetMessageOnUserDerivedErrorintests/errors/try_catch_test.gb.- Cross-type casts:
string <-> bytesandlist <-> set."hello" as bytesencodes UTF-8; abytesvalueas stringdecodes UTF-8 (the cast raises a catchableRuntimeErrorif the byte sequence isn't valid UTF-8).[1, 1, 2, 3] as set<int>deduplicates (first occurrence wins);{1, 2, 3} as list<int>materializes in unspecified order. Previously each of these raised "cannot cast X to Y". The element-type generic argument is required for the collection casts to match the typed-declaration rules. New parity testTestParityCrossTypeCastsForBytesAndCollections; new test classesStringBytesCastTestandListSetCastTestintests/core/cross_type_casts_test.gb. - New
stringmodule: factory and static helpers. A small namespace for things that don't fit as instance methods on a string value:string.fromCodePoint(n)-> single-character string for Unicode codepointn. Rejects negative values, codepoints above U+10FFFF, and the UTF-16 surrogate range (U+D800..U+DFFF). Counterpart to the existing.codePointAt(i)instance method.string.fromCodePoints(list<int>)-> multi-character string built from a list of codepoints, same validation per element.string.compare(a, b)-> three-way comparison returning -1 / 0 / +1. Useful as a sort key (xs.sortBy(string.compare)). Java / Go convention.string.equalsFold(a, b)-> case-insensitive equality respecting Unicode case folding ("CafÉ" == "café"). For timing-attack-safe equality (HMAC verification, token comparison) usesecrets.constantTimeEqualinstead; the string-module helpers are not constant-time. New parity testTestParityStringModule; new test classStringModuleTestintests/core/cross_type_casts_test.gb.
null as ?Tis a successful cast on both backends. Casting anullvalue to a nullable type previously errored with "cannot cast null to T" on the evaluator (the cast path dropped the nullable bit from the target TypeRef before calling castValue). The VM accepted it after the 1.0.2 cast-error catchability work but the eval side still failed. The evaluator's CastExpression handler now special-cases a nullable target ahead of the class-chain match, mirroring the VM. New parity testTestParityNullAsNullableType; new test classNullableCastTestintests/core/cross_type_casts_test.gb.- Method-dispatch hot path: name-lowering and classInfo lookup
caches. Two small VM-side memoising caches reduce per-call
overhead on tight method-call loops.
nameLowerCacheskips the repeatedstrings.ToLower(methodName)on every dispatch (the chunk'sClassInfo.Methodsis keyed lowercase, so the lowered form is what the lookup actually needs).classInfoNameCacheskips thevm.classIndexlookup when the receiver'sinstance.Class.Namehas already been resolved. Measurable on theclass_dispatchextended benchmark (~12% improvement, 42ms -> 37ms median for 50000 calls); numeric / list / string benchmarks unchanged.
Bug fixes
- VM closure capture for camelCase identifiers. The compiler's
free-variable scanner lowercased identifier names while local
scope entries kept their original case, so closures that captured
a variable with uppercase letters silently missed the capture.
The closure body then read the wrong stack slot at runtime,
producing puzzling type errors. Both
freeVarSetand the enclosing scope are now case-sensitive throughout. New parity testTestParityClosureCaptureCamelCase. nullmatchesanyin VM method overload resolution. The VM rejectedobj.send(null)whensendwas declared asfunc send(any body)- the early null-vs-nullable check fired before thevmTypeAnyshort-circuit, so the overload selector reported "no matching overload". Evaluator was already correct. New parity testTestParityNullMatchesAnyParam.- Cross-module typed-parameter dispatch. A function declared
with a parameter typed as a module-qualified class (e.g.
func f(appmod.GebwebApp app)) failed at the dispatch boundary because the runtime kept the qualified name. The strip-prefix path now applies uniformly so a stdlib facade can declareappmod.GebwebAppparameters and accept values built by the same module. reflect.class(name)finds user classes from stdlib modules. When called from inside an imported module (e.g.gebweb.binding),reflect.class("UserDTO")previously returned null because the module loader only scanned imported modules' chunks. The loader now also scans the main chunk, so framework code can resolve user-script classes.- Cross-chunk deserialize.
json.parseAs(text, UserDTO)from inside a stdlib module crashed with "class index out of range" because the VM tried to resolve the user class's index against the wrong chunk. The deserialize path now dispatches via the module loader to a sub-VM bound to the right chunk, so the framework can deserialize main-chunk DTOs from binding helpers. - Cross-VM exception propagation. A throw originating in user
code that bubbled across a sub-VM boundary collapsed to a plain
"uncaught RuntimeError" string at the boundary, losing the
original class and parent chain.
catch (errors.HttpException e)in a stdlib closure no longer matched the originalNotFoundError. The VM now wraps the underlyingruntime.Errorin avmThrownErrorso the calling VM can recover it viaerrors.Asand re-throw it as a typedpendingThrow. New parity testTestParityCrossModuleThrowCatch. aswidens to a parent class on both backends. The cast operator previously rejected widening an error- or instance- derived value to a declared parent (e as errors.HttpExceptionfor aNotFoundError). Both backends now walk the parent chain the same wayinstanceofdoes and treat the cast as a no-op when the value is already an instance of the target.- Eval
reflect.method(...)()preserves module access. A bound method returned byreflect.methodran on a fresh stub Evaluator with no module loader, so the method body couldn't reference any imported module (gebweb.notFound(...),json.stringify(...), ...). The bound Native closure now captures the live host Evaluator. New parity testTestParityReflectMethodPreservesModuleAccess. - Parenthesized selector forces value-then-call semantics.
(obj.fn)(args)previously parsed identically toobj.fn(args)and dispatched as a method call onobj, ignoring the parens. The parser now flags the SelectorExpression so the evaluator and the VM both invoke the VALUE ofobj.fn(a closure stored in a field, a method reference, anything callable) instead. Required for caching callables on instance fields -let response = (this.dispatch)(request);. New parity testTestParityParenthesizedSelectorInvokesValue. reflect.className(value). New reflect builtin returning the class's own identifier. For a class value, returns its name (reflect.className(User)returns"User"); for an instance, same asreflect.typeOf(instance); for primitives, the runtime type name ("int","string"). Symmetric withreflect.class(name)going the other way. Required by gebweb's DI container to identify a service by its class without instantiating it. New parity testTestParityReflectClassName.- Class-ref runtime construction via
classRef(). A class value passed through a variable or obtained fromreflect.classis now callable to construct an instance - previously the VM treatedclassRef()as a static-method lookup of__invokeand errored with "unknown static method ... __invoke". Both backends now construct the class (routing through the moduleLoader when the class was declared in a different chunk). Required bygebweb.app([HelloController])to instantiate user controllers via DI. New parity testTestParityClassRefRuntimeConstruction. - Cross-chunk
reflect.constructors. When given a class value declared in another chunk, the VM now dispatches through a newConstructorsForModuleClassloader hook so the metadata reflects the originating chunk's constructor list rather than the caller chunk's stale view. Same pattern as the deserialize and construct module-class hooks. - Dotted decorator names.
@Foo.bar,@Foo.bar.bazand longer chains parse as a single composite identifier. The whole dotted string is the decorator's name; dispatch is by exact string match. Lets framework-style families like@Assert.email,@Assert.minLength(2)group related rules under a common prefix without naming-collision worries. - Field-level decorators.
@-prefixed annotations on field declarations inside a class body now parse and persist intoreflect.fields(class)as a per-fielddecoratorslist. Field decorators are pure metadata - the runtime never executes them automatically; frameworks read them via reflection to drive validation, serialisation filters, OpenAPI enrichment, etc. See Classes And Interfaces > Decorators in the manual for the semantics. New parity testTestParityFieldDecoratorsAndDottedNames. - Chunk format Version 49 to 50. New per-field decorator-
metadata list parallel to
FieldNamesso reflection over cross-chunk classes returns the field annotations from the declaring chunk. - Bare
return;in avoidfunction. The static analyzer previously rejectedreturn;inside a function declared as returningvoid, raising "cannot return null from F returning void". An early exit from a void body is legal - there is no value being returned, only an early termination. Both the evaluator and the VM already handled this at runtime; only the analyzer needed adjusting. New parity testTestParityBareReturnInVoidFunction. - Trailing comma in list literals.
[1, 2, 3,]and multi-line variants now parse. Dict / set literals already supported it; list literals catch up. New parity testTestParityTrailingCommaInListLiteral. - Class declaration inside a block is rejected at parse time.
Previously
class X {...}inside a method body parsed but produced confusing downstream failures. Now emits a clear "class declaration is only allowed at the top level" error. Same forinterfaceandenum. - Lexical shadowing wins over module dispatch. When a local
variable shadows a built-in module name (e.g.
let errors = [...]while theerrorsmodule is loaded), the receiver of anX.method(...)call now resolves to the local rather than the module. Eliminates a class of confusing "X is not a module" runtime errors. Both backends. The analyzer also emits a warning (not an error) at the shadowing declaration so a reader doesn't have to wonder why a familiar module name is bound to a list. New parity testTestParityLocalShadowsBuiltinModule. reflect.getField(instance, name)andreflect.setField(instance, name, value)native builtins. Dynamic field-by-name access / assignment for framework code (Gebweb's@Assertvalidator walks an instance's fields by name; the@ApiResourcePATCH handler updates an entity field-by-field). Replaces the previousjson.parse(json.stringify(instance))round-trip workaround. Both backends. New parity testTestParityReflectGetFieldSetField.- Forward function references in the bytecode compiler.
Previously
func a() { return b(); } func b() { ... }failed with "no matching overload for b" becauseb's parameter / return metadata wasn't populated until its body was compiled. The compiler now pre-populates function signatures during the initial sweep so call sites see the real shapes. New parity testTestParityForwardFunctionReferences. - Cross-chunk
reflect.fields(instance)preserves field decorators. Previouslyreflect.fields(instance)on a value handed to a sub-module returned no decorator info because the originating chunk's ClassInfo wasn't reachable. The instance'sruntime.Class.Fieldsis now populated at construction time with each field's name and decorator metadata so framework code in another module sees the same annotations the declaring module does. The chunk-local path still wins when available (full type strings); the new path is the fallback for cross-chunk reflection. New parity testTestParityCrossChunkInstanceFields.
Bytecode
- Chunk format Version 48 -> 49. New per-field type strings
parallel to
FieldNamesso cross-chunk reflection on classes produces the same shape as the evaluator without consulting the source AST.
Other
- Build: still Go 1.26.3.
- VS Code extension: bumped to 1.0.2 for parity.
1.0.1 (2026-05)
A correctness release that closes two related holes in the 1.0 generics story.
Highlights
- Generic invariance - User-defined generic class types are now
invariant in their type parameters. Even when
Sub extends Base, aBox<Sub>is not assignable to aBox<Base>parameter or typed variable. The static analyzer rejects the assignment at compile time; the runtime rejects it at the function-parameter boundary when the value's reified bindings disagree with the declared bindings. This is the standard invariance rule that Kotlin/Java/C# enforce, and it eliminates the classic unsoundness where a function widensBox<Sub>toBox<Base>and then inserts a sibling subtype. See chapter 3's Generics: invariance. - Explicit type-argument call syntax -
ClassName<T>(args)andfuncName<T>(args)now parse correctly. Before 1.0.1 the parser treated<and>as comparison operators in these positions, soBox<int>()failed with a syntax error andassertIs<string>("x")silently compiled into a chained comparison that exploded at runtime. The new lookahead disambiguates: an identifier followed by<TypeRef (, TypeRef)*>immediately followed by(is a generic call; everything else stays a comparison. This is the form needed to write the invariance check above:Box<Sub> b = Box<Sub>();
Tooling
- VS Code extension bumped to 1.0.1.
Build
- Documentation now references the Go 1.26.3 toolchain (matching
go.mod) as the minimum supported build environment.
1.0.0 (2026-05)
The first stable release. Everything documented in this manual is in scope for the 1.0 stability promise: source-level syntax, stdlib APIs, runtime semantics, and the bytecode chunk format. Future 1.x releases will add features but not break what is below.
Highlights
- Destructors and context managers (two separate concerns) -
func ~ClassName()declares an end-of-lifetime hook. Destructors fire at program exit (the runtime sweeps every destructor-bearing instance that hasn't already been destroyed, in reverse-creation order) or via an explicitdel x;statement, which retires the binding and invokes the destructor inline.delaccepts an identifier only; the semantic analyzer flags any reference to a destroyed binding withuse of destroyed binding "x"so the type system stays sound. The unrelatedwith (resource) { ... }block is the context-manager construct: it calls__enter__()on entry and__exit__()on exit (any exit path - normal, exception, return, break, continue) but does not invoke the destructor. See chapter 6's Destructors and Context Managers sections. - Class (de)serialisation -
json.stringify(instance)(and the YAML / TOML equivalents) accept user-defined class instances and dump their public fields by default (_/__prefixed names are private and skipped). Classes can override with__serialize__()(any returned dict / list / scalar is recursively serialised). The symmetricjson.parseAs(text, ClassRef)reconstructs an instance, preferring a static__deserialize__(dict)factory when defined and falling back to positional constructor calls matched on parameter names. Same foryaml.parseAs,toml.parseAs,xml.parseAs. See chapter 6's Serialisation. - Async combinators -
async.all([tasks]),async.race([tasks]),async.timeout(task, ms), andasync.cancel(task)(also reachable viatask.cancel()and thetask.cancelledproperty). The pre-1.0 "cancellation and structured scheduling remain roadmap items" caveat is gone. See chapter 9. - Scheduling primitives -
time.scheduler.Timer,time.scheduler.Ticker, andtime.scheduler.Intervalgive callback- style scheduling with cancellation. UsesetTimeout/setIntervalaliases if you prefer the JavaScript naming. - Symmetric encryption -
crypt.aesEncrypt/crypt.aesDecrypt(AES-256-GCM) andcrypt.chacha20Encrypt/crypt.chacha20Decrypt(XChaCha20-Poly1305) join the existing hash / HMAC / Argon2id / RSA / EC stack. Seestdlib/12-security.md. - Reusable HTTP clients -
http.newClient({...})now acceptscookieJar,keepAlive,maxIdleConns,proxy, andproxyFromEnvoptions for production-shape HTTP usage. Session flows that need to retainSet-Cookieacross requests just pass"cookieJar": http.newCookieJar(). Default User-Agent is nowGeblang/1.0(override viaheaders). - Improved regex API -
re.matchreturns a clean{text, groups, named}dict instead of ad-hoc numeric-string keys, andre.matchAlliterates every non-overlapping match. Seestdlib/09-text.md. - Wider encoding coverage -
encoding.base32Encode/base32Decode(RFC 4648, padded or unpadded) andencoding.base58Encode/base58Decode(Bitcoin / IPFS alphabet, preserves leading zeros). Accepts both string and bytes inputs. .lengtheverywhere -list,dict,set,string,bytes, andrangeall expose.lengthas a property (alongside the existing.length()method).- Inherited generic type bindings -
class Sub extends Base<string>now propagatesT to stringto subclass instances, visible viareflect.typeBindings(instance)["T"]. funcas a field type -class Holder { func cb; ... }parses correctly.callableandfunctionwork too.- Module top-level discipline - files that begin with
module name;now require declarative top-level statements only (import,export,const/let/ typed declarations,func,class,interface,enum,typealias, and at most oneinit { ... }block). Free-standing calls,if/while/for/match/try, and bare assignments are rejected with a clear diagnostic; the rule does not apply to script files. Seedocs/user/07-modules-packages.md. - Static errors abort
geblang run- the previous behaviour printedwarning: ...for static-analysis errors caught by the bytecode compiler (e.g. no matching overload, type mismatch), fell back to the evaluator, and crashed partway through the run. Now those errors abort the run cleanly before any statement is executed. Genuine compiler gaps (the bytecode compiler doesn't yet support a feature) still fall back silently.geblang checkoutput is the source of truth for what blocksgeblang run. - Diagnostic severity -
semantic.Diagnosticnow carriesSeverity(Error / Warning). All existing checks remain at Error by default; the field is in place so future analyzer passes can emit Warning-level findings that surface in VS Code's Problems panel and ingeblang checkbut don't block execution. - Aliased native imports compile on the VM - the bytecode
compiler now recognises
import path as natpath; natpath.clean(...)and dispatches to the canonicalpath.clean(...)directly. Previously these calls failed to compile withunknown bytecode name natpathand fell back silently to the evaluator; under--vm-strictthey were rejected outright.stdlib/pathlib.gbandstdlib/schema/validator.gbnow also run on the VM rather than via fallback. Unknown-identifier failures from the bytecode compiler are no longer treated as parity gaps and abort the run along with the other static errors. - Autocomplete for primitive-type methods - typing
someDecimal.<TAB>afterdecimal d = 3.14;now surfacesformat,abs,toString, etc. The LSP server's completion path detects the receiver type via a light lexical scan (<primitiveType> <name>declarations) and looks up the methods in a per-type table. Coversstring,int,float,decimal,bool,bytes,list,dict,set, andrange. Inferred declarations (let x = ...) and complex annotations (?string,list<int>) are best-effort; the catalog can be extended later if needed. - DAP launch pre-flight - VS Code's Run Without Debugging
(Ctrl+F5) and Debug launches both went through the DAP server's
evaluator-only path, which skipped the static-analysis pre-flight
the CLI run path performs. A script with a static error
(no-matching-overload, type mismatch, undeclared identifier,
module-top-level violation) would run partway through before
crashing in the Debug Console, instead of aborting cleanly. The
DAP server now runs the same semantic + bytecode pre-checks as
geblang run; static errors abort the launch and surface the diagnostic in the Debug Console.bytecode.IsParityErroris exported so cmd/geblang and internal/dap share the same parity-gap classifier.
Toolchain
- Go 1.26.3 build - Docker image and
go.modnow target Go 1.26.3. Dependency versions refreshed (pgxv5.5 to v5.9,sqlitev1.29 to v1.50,golang.org/x/cryptov0.17 to v0.51).scripts/upgrade-go.shinstalls the matching toolchain on Ubuntu/WSL. - Vet-clean - audited and fixed 181 printf-style call sites that Go 1.26's promoted vet check flagged in parser / semantic / vm.
- Cleaner build output -
make docker-build/make vscode-buildno longer print spurious "Error response from daemon: No such container" lines on first runs.
Performance
Geblang's bytecode VM remains competitive with the reference interpreters. Measured on this build (median of 7 runs; lower is better):
| Benchmark | Geblang | Python | PHP |
|---|---|---|---|
| numeric_loop | 122 ms | 135 ms | 29 ms |
| recursive_fib | 83 ms | 46 ms | 22 ms |
| list_pipeline | 7 ms | 14 ms | 11 ms |
The 1.0 round of work was deliberately feature-focused; the post-Phase-O
performance baseline holds. See benchmarks/run.py for the harness.
Bug fixes worth flagging
- VM
??infinite loop:value ?? defaultinside an async-run callback or HTTP handler used to enter an infinite loop becauseOpNullCoalescewas missing from the bytecode VM's jump-shift list. Same fix for?.(OpOptionalChain). Fixed; both paths now have parity tests.
Migration from 0.9.x to 1.0
If you have code written against a pre-release Geblang build, two changes need source updates:
-
re.matchresult shape- let m = re.match("(?P<name>\\w+):(\\d+)", text); - io.println(m["0"]); // full match (string key "0") - io.println(m["1"]); // group 1 - io.println(m["name"]); // named group + let m = re.match("(?P<name>\\w+):(\\d+)", text); + io.println(m["text"]); // full match + io.println(m["groups"][1]); // group 1 + io.println(m["named"]["name"]); // named groupre.findAllwas unchanged. The newre.matchAllreturns a list of the new-shape dicts for iteration. -
Default User-Agent
Outgoing HTTP requests used to inherit Go's
Go-http-client/1.1. They now sendGeblang/1.0. If a server allow-lists by User-Agent, update its rules or passheaders: {"User-Agent": "..."}.
Everything else - syntax, control flow, classes, async, generators, stdlib calls, decorators, error model - is source-compatible with the last pre-1.0 build.
Bytecode chunk format
Chunk Version bumped to 48 (was 46) to accommodate the new
ParentArguments slot and the DestructorIndex slot on ClassInfo.
Compiled .gbc artefacts from 0.9.x will not load on 1.0; rebuild
them with geblang 1.0 or run geblang cache clean.