Changelog
All notable changes to Objeck will be documented in this file.
Contents
- [v2026.10.3] - 2026-11-03
- [v2026.10.2] - 2026-10-31
- [v2026.10.1] - 2026-10-20
- [v2026.10.0] - 2026-10-11
- [v2026.9.7] - 2026-09-28
- [v2026.9.6] - 2026-09-19
- [v2026.9.5] - 2026-09-19
- [v2026.9.4] - 2026-09-15
- [v2026.9.3] - 2026-09-14
- [v2026.9.2] - 2026-09-13
- [v2026.9.1] - 2026-09-12
- [v2026.9.0] - 2026-09-03
- [v2026.8.4] - 2026-08-30
- [v2026.8.3] - 2026-08-19
- [v2026.8.2] - 2026-08-16
- [v2026.6.4] - 2026-06-28
- [v2026.6.3] - 2026-06-27
- [v2026.6.2] - 2026-06-19
- [v2026.6.1] - 2026-06-14
- [v2026.6.0] - 2026-06-07
- [v2026.5.4] - 2026-05-28
- [v2026.5.3] - 2026-05-24
- [v2026.5.2] - 2026-05-17
- [v2026.4.3] - 2026-04-12
- [v2026.4.2] - 2026-04-06
- [v2026.4.1] - 2026-04-05
- [v2026.4.0] - 2026-04-05
- [v2026.2.1] - 2026-02-26
- [v2026.2.0] - 2026-02-12
- [v2025.7.0] - 2025-07-01 ✅
- Previous Releases
- Versioning
[v2026.10.3] - 2026-11-03
A hardening release: the garbage collector now knows exactly which words of compiled code are references on x64 and ARM64, the loader is fuzzed and verifies programs before it runs them, errors are returned instead of ending the host process, and the HTTP client stops running Windows out of ports.
.obe and .obl file formats changed (magic 0xffbf for programs, 0xffba for libraries) because an Int[] and an object array are now different heap kinds: recompile your programs and rebuild any library of your own with this toolchain. A program or library built by an earlier version is refused with a message saying so.Runtime and garbage collector
- Compiled code tells the collector exactly which words are references (#1496, #1504, #1506, #1511; design in
docs/GC_ROOTS_DESIGN.md): a JIT frame now has typed spill slots, so a collection reads exactly the words the code marked as references. AnIntequal to a moved object's address, pending in a compiled method, is no longer rewritten. The conditional (and/or) temp is carried through a reference slot too, and the dead AMD64 inliner is gone. Measured 6,171 stress runs on each of five platforms with no failure. - An
Int[]has its own heap tag (#1510): anInt[]element that happened to equal a moved object's address was rewritten by a minor collection, interpreted and compiled alike (242 of 2,000 in the reproduction). Object, class, string and function arrays keep the scanned tag. This is why the file formats changed. - Unresolved call targets and worker-thread exceptions are reported (#1494): a missing class or method in a call chain, and an exception on a worker thread, no longer end the process silently.
- The native library ABI is versioned (#1492):
VMContextcarries an ABI version and its own size, and thelib_api.hhelpers areinline.
Loader and compiler
- The loader is fuzzed, and every program is verified before it runs (#1508, #1512;
tools/fuzz/loader,docs/SECURITY_MODEL.md): hundreds of millions of executions found and fixed a crash on a repeated class or method id, allocations of up to 16 GB from a corrupt count, an abort on a bad method signature, a silent exit on an unknown opcode, a read past the end of a.obeshorter than four bytes, and leaks on every refused load. A verification pass checks jump targets, local and trap ids, memory contexts, array dimensions, and class, method and field ids, with no measurable startup cost. The loader returns its errors (23exit(1)calls removed);obrprints the same message and exits 1. - A fatal compiler error no longer ends the host (#1507): the compiler back end throws instead of calling
exit(1), so the language server stays up when a library fails to load. The command line prints the same text and status (54 failure cases compared byte for byte). - One diagnostic record (#1509): parser and analyzer errors share one
Diagnosticand sink; the language server reads the fields instead of splitting strings.
Networking
HttpClientreuses connections (#1529, fixes #1443, #1503 and #1516; likely also #1461): a request used to open a connection and close it, and the side that closes first holds the local port inTIME_WAITfor about two minutes; on Windows a process making about 130 requests a second used all 16,384 ports and every connect then failed at once. Plain-HTTP connections now wait in a pool (16, reused within 4 s) when the server sayskeep-aliveand the response ends where its own framing says; a GET or DELETE on a reused connection that answers nothing goes once more on a new connection (RFC 9112 9.3.1). POST, PUT and PATCH never reuse a connection, and the newHttpClient->CloseIdleConnections()(called byWebServer->Stop()) closes the waiting ones. Hosted Windows runners: 4 of 4 failing harness runs became 4 of 4 clean, with lingering ports down from 16,180 to 63; the nightly on the merge commit passed on all five platforms.
Libraries
- A failed request or an empty reply no longer dereferences
Nil(#1490, #1527): OpenAI, Gemini, misc and net first, then JSON, XML, RegEx, RSS, JSON-RPC and the web server, with tests. RegExrepeat counts are bounded (#1534):a{99999999999}filled the heap while compiling and never returned. A count is now at most 1000, the low bound may not exceed the high one, and a pattern that expands past 100,000 instructions is not OK and matches nothing.- The JSON-RPC client sends a fresh request id and checks the reply (#1534): the id was a shared static that never advanced. A reply that echoes another id is
Nilwith error -32600. WebDownloaderno longer skips URLs (#1535): given seven or more URLs, the position overshot after the first group of three and the seventh was never fetched; its result came back empty with no error.Collection.Pair->Comparecompares the other pair's first value (#1526): sorting a vector of pairs ended in 'Invalid object cast'.
Language server
- Edits analyze faster (#1531, #1532; part of #1236, design in
docs/INCREMENTAL_ANALYSIS_DESIGN.md): the diagnostics library inflates each library once per process instead of on every edit and no longer builds library instructions it never uses, and a folder's analysed program is kept until a newer one replaces it. On Windows a hover after an edit in a small workspace went from 165 ms to 53 ms, and a hover in a second folder from about 600 ms to about 60 ms. The cached and uncached servers gave identical answers for every file in more than 6,000 edited texts;OBJECK_LSP_NO_CACHE=1keeps the uncached server as a reference.obcoutput is unchanged. Phases 3 and 4 (re-parse only the edited file, repeatable analysis) are not started.
Debugger
- The debug adapter keeps output whole, orders messages, and refuses what it does not understand (#1487, #1495): partial UTF-8 is carried across reads, sequence numbers are ordered under one lock, an unknown request fails instead of being ignored, you can pause, and breakpoints keep their file and have their own ids. A lock-free fast path cut a breakpoint-heavy run's hook cost from 25.3 s to 0.25 s on four threads.
Testing, CI and documentation
- The nightly hardening run is scheduled, and the cloud checks run on your machine first (#1489, #1484, #1493, #1502, #1514): actions are pinned by commit, the Linux legs are pinned to Ubuntu 24.04, and
tools/cicd/local_linux.shbuilds the committed tree, compiles every library from source and fails when the flag tests crash. - Four intermittent test failures found their causes (#1505, #1513, #1515): three TLS tests made a certificate valid only from the second it was created (a clock step made it 'not yet valid'), a JSON-RPC test chose a port still held by an earlier connection, and
threads_12was retried and judged by exit status alone. - New documents: the architecture assessment (
docs/ARCHITECTURE_ASSESSMENT_2026_11.md), the VM trust model (an.obeis native code),docs/COLLECTIONS_GUIDE.md,docs/ERROR_CONVENTIONS.mdanddocs/ADDING_AN_INTRINSIC.md. - Getting-started guides and examples (#1540,
docs/guides,programs/examples/guides): eight guides (Query.Structured, the HTTP client and server, JSON/XML/RegEx, collections, AI, the language server, and what is new in 10.3), fifteen runnable programs, an Examples section in the README, and eight matching pages on the site. Every code block and output block in a guide is byte-for-byte a program and its real run, andtools/docs/guides_to_html.py --checkfails when a site page is stale. - An architecture map and an index of the docs (#1541,
docs/ARCHITECTURE_MAP.md,docs/README.md): one page that shows how the compiler, file formats, VM, JIT, garbage collector, libraries, tools and pipeline fit together, an index of every doc with a status checked against the code, and short READMEs forcore/shared,tools/cicdandprograms/tests. Several docs that disagreed with the code were corrected (the linker's place in the compiler, the removed AMD64 method inliner, the test counts, where VM options are parsed).tools/cicd/check_doc_links.pynow checks every relative link and anchor in the Markdown files in CI, anddocs/DOC_COVERAGE.mdrecords that 6,610 of 7,299 public library members (91%) have a doc comment. - A checkers game with ten AI levels (#1528, #1539,
programs/examples/checkers): an SDL2 example with minimax and Monte Carlo opponents fromSystem.AI, a self-play learner, ten piece sets (two of them tech-neo, rubber-duck debuggers against coffee mugs and glitchy ghosts against rebooting robots, on circuit-board boards), a flat or slanted camera, generated music in five styles (hip-hop, modern beats, reggae, techno and classical) with a deep-bass setting, animation and an optional fun mode. The move generator is checked against the published perft numbers, and a 439-check headless test runs in the regression suite.
Upgrading
- Recompile every
.obeand rebuild every.oblof your own: the program and library magic numbers moved to0xffbfand0xffba. - A malformed
.obeis now refused by the verifier with a message; a valid program is never refused. HttpClientmay keep a connection open between requests; code that stopped a server and started another on the same port within four seconds and relied on a fresh connection should wait for the old server to exit.
[v2026.10.2] - 2026-10-31
Objeck gains a database in the standard library: Query.Structured is now a columnar engine with SQL joins, grouping, indexes and expressions over typed in-memory tables, files and URLs named in FROM, a crash-safe .otab file format, and sources that read CSV, JSON, XML, text logs, filesystems, zip and tar archives, Excel workbooks, ODBC databases, System.ML results and image detections as tables. The runtime collects in proportion to live data and allocates without searching, Collection.Hash is open-addressed with unboxed IntVector/IntMap/BitSet beside it, calls nest 16,384 deep, DTLS works for the first time, every socket failure says why, servers take IPv4 and IPv6 clients on every platform, and Windows handles non-ASCII arguments, environment variables, paths and console text. System.ML gains kernel and one-class SVMs, six outlier detectors and an autoencoder, a new System.ML.Lab for experiments, result tables and charts, and BERT sentence embeddings over ONNX; Data.JSON decodes escapes when it parses, so a document reads back as it was written; the JIT compiles code it used to hand back to the interpreter and no longer loses a value at a loop's collection safepoint; the collector keeps a conditional's value alive across another thread's collection; and the language server analyzes each edit once, about twice as fast. Probing six libraries and the compiler fixed defects in CSV, JSON, regular expressions, NLP, ML, Ollama, OpenAI, Gemini, SDL2, ODBC, numeric literals, name resolution and array literals. The first AI-foundation pieces arrive: System.NLP.Onnx.TextClassifier runs a Hugging Face text classifier as Python's pipeline does, System.AI.Hub.HubClient downloads models by name into a verified cache, BpeTokenizer matches RoBERTa's and GPT-2's tokenizer id for id, and GetToFile streams any download to disk; a redirect to another host no longer receives the caller's credentials; and objects keep their instance IDs when the collector moves them, so Hash and Map keyed by objects keep their entries.
.obl is a new format (magic 0xffb9): rebuild any library of your own with this toolchain. The full list of behaviour changes is in the Upgrading section at the end of this entry; the ones most likely to touch a program are an .obe from an earlier release being refused (recompile), Directory->List no longer returning . and .. on Windows, a leading-zero integer literal (010) being a compile error with 0o10 as octal, Byte->CompressBr renamed CompressDeflate, Data.JSON decoding escapes when it parses (stop calling JsonElement->Decode), TF_IDF->New() smoothing and normalizing by default, Byte[]->ToHexString returning a digest, Hash iteration order changing, and a call stack that defaults to 16,384 frames rather than 256.Query.Structured
- A columnar engine, designed first and built in phases (#1295, #1298, #1301, #1303, #1325; design and as-built notes in
docs/QUERY_DESIGN.md): every column is a typed array (INT, FLOAT, STRING, DATE, BOOL, OBJECT) with a null bitmap, text is dictionary-encoded, and each 65,536-row group carries min/max zone maps so a filter skips groups it cannot match. A query returns aView, a selection of row numbers with nothing copied;Where,Select,OrderByandLimitchain on it. Awhereis compiled once into typed predicates (Col("value")->Gt(0.5)->AndAlso(...)), prepared statements take?placeholders, and a table caches 64 parsed plans. The SQL front end is a new recursive-descent parser with contextual keywords:INNER/LEFT JOIN ... ONthrough aCatalog,GROUP BYwithCOUNT/SUM/AVG/MIN/MAX/MEDIAN/STDDEVandHAVING, multi-keyORDER BY ASC|DESC,LIMIT/OFFSET, multi-columnDISTINCT,MATCHESoverQuery.RegEx, andINSERT/UPDATE/DELETE/CREATE TABLEapplied in full or not at all. The old API (Table->New(name, String[]),Insert() ~ Row,Query(sql) ~ Table,FromCsv,FromFilesystem,Finder) keeps its signatures and results. Measured at 100,000 rows on the Windows x64 development machine, against the v2026.10.1 library:where value > 0.50.558 s to 0.023 s,order by3.16 s to 0.096 s,Sum0.051 s to 0.004 s, 10,000Get(pk)0.032 s to 0.005 s; a join with a 5,000-row table 0.11 s,GROUP BYinto 5,000 groups 0.11 s. - Files and URLs are tables (#1303, #1366, #1384, #1391): a quoted path in
FROMorJOINis read by its extension (.csv,.tsv,.json,.jsonl,.xml,.otab, each also.gz),csv('data.txt', delimiter => ';', header => false)and the other function forms set options, a glob reads several files as one table with a_filecolumn, andCatalog->Run(sql)is the stateless one-off form. Anhttp://orhttps://path is fetched through a download cache revalidated withETagandLast-Modified. The cache is correct across processes that share the temporary folder: each URL has one folder of immutable versions, each written once under a name no other writer uses, and one small note naming the current version with its validators, published by a single atomic rename, so the validators a reader sends always belong to the copy it reads and a 304 means that copy is current. Before, a Windows writer could not replace a copy another process had open (the query failed withcannot write its copy), and two processes that downloaded different versions could leave one's copy beside the other's note, so an older file was served as current; superseded versions are deleted after a five-minute grace period with no lock file, and the old layout is not read, so the first fetch of each URL after upgrading downloads it once more. An archive's member or a workbook's sheet follows the path after a dot, asschema.tabledoes:select * from 'results.zip'.'2026/runs.csv',from 'budget.xlsx'.'Q1 2026',from 'runs/*.tar.gz'.'metrics.csv'. Each catalog caches files by size and modification time, and a file read within a second of its last change is also held with a digest of its bytes, so one rewritten in the same second to the same size is read again (File->ModifiedTimehas whole-second resolution, and a finer clock does not help because file systems stamp with a coarse tick);Catalog->SetFileAccess(false)refuses them all, which is whatTableServerruns with. - Sources (#1298, #1301, #1303, #1329, #1331, #1334, #1350, #1352): a
Sourcedeclares its tables and fills a catalog, with aScanHintfor the columns, filter and row limit a query needs. Inquery.obl:CsvSource(whole-column typing, RFC 4180 throughData.CSV),FilesystemSource(flat or recursive, with_id/_parent_id/depth),ObjectSource<T>(aVectorof the program's objects),TextSource(regular-expression capture groups become typed columns; multi-line records; unmatched lines counted),JsonLinesSource,JsonSourceandXmlSource(nested repeated parts become child tables joined on_parent_id = _id),HttpJsonSource(records at a dotted path, paged by a next link or an offset),ArchiveSource(zip, tar and gzipped tar read from a file, URL or bytes, with every member in a readable format as a table; nothing is extracted, and.., absolute paths, duplicate or overlapping zip members, zip bombs and CRC mismatches are refused), andXlsxSource(each worksheet as a typed table: shared and inline strings, booleans, error values, dates in both the 1900 and 1904 systems with the Lotus leap-year serial undone, merged cells kept as Excel stores them or filled withSetFillMerged). In their own libraries, so a program that only queries files loads no native module:query_ml.obl'sMlResultSource(folds, out-of-fold predictions, threshold sweeps, confusion matrices, bootstrap intervals and per-group recall fromSystem.ML, withMlColumnsas the bridge back to a matrix),query_odbc.obl'sOdbcSource(a table or query from any ODBC database, typed from the driver's description, with columns, a row limit and aSetWherecondition pushed into the SQL it sends, names always quoted and values always bound), andquery_vision.obl'sVisionSource(a folder of images and a detector's boxes asimagesanddetectionstables, with image headers read in pure Objeck andYoloDetectoroverAPI.Onnx). - The
.otabfile (#1315, #1321, #1365; byte-exact specification indocs/OTAB_FORMAT.md, an independent reader intools/otab/otab_dump.py):table->Save(path[, compression]),Table->Load,ToBytes/FromBytes, andAppend, which adds row groups without rewriting the existing ones. Every block and the footer carry a CRC32, a save writespath.partand renames it into place, andOtabFilereads only the footer and then only the columns aReadnames, over HTTP range requests as well as from disk.Compression->FAST/SMALLare zlib levels 1 and 9 per block, stored raw where compressing would not shrink. A table opened withTableStore->New(path)orTable->Open(path)is durable: every statement is appended to a change log before the call returns, a torn last record is dropped while a damaged one refuses the open with its offset,Checkpoint()folds the log into a save, and a writer lock holding the process id refuses a second writer and clears a dead one's. Index definitions are kept in format 2, written only for a table that has indexes, so a table without them is byte for byte what v2026.10.1 wrote. - Hash and sorted indexes, and a planner that shows its work (#1351, #1364):
table->CreateIndex(column, IndexKind->HASH | SORTED),DropIndex, and in SQLCREATE INDEX [name] ON t (col) [USING HASH | SORTED]andDROP INDEX. A hash index serves=andIN; a sorted index serves ranges,BETWEEN,ORDER BY,GROUP BYon one table andMin/Max. An index lookup returns candidate slots and the whole condition is then tested on them in slot order, so results and their order are a scan's by construction, which a differential test holds it to.View->GetPlan(),Statement->Explain(),Table->Explain(sql)andCatalog->Explain(sql)report steps such aswhere runs: HASH index runs_grp on grp (=). At 1,000,000 rows (Windows x64,-opt s3), equality on an Int 73 ms to 0.02 ms, a 0.1% range 109 ms to 0.7 ms,ORDER BY value LIMIT 102.8 s to 0.08 s; building an index takes 0.2 to 0.6 s. Several threads may query one table at once, including right after a write: a stale index, zone map or plan cache is rebuilt under one process-wide lock taken only on that slow path. - SQL expressions in every clause (#1367, #1398, #1416, #1417; design in
docs/QUERY_DESIGN.md, #1395):+ - * / %, unary minus and parentheses over columns, numbers, text, dates,TRUE/FALSE,NULLand?placeholders work in the select list, inWHERE(on either side of every test, inINlists andBETWEENbounds, and inUPDATEandDELETEconditions),ORDER BY,GROUP BYandHAVING, inside aggregates (sum(price * qty)), over aggregates (sum(a) / count(*),max(x) - min(x)) and inJOIN ... ON(on t.n + 1 = u.w, a range join withbetween, a key plus another test,OR, a join over two earlier tables). A parsed expression is never changed, so cached plans stay shareable between threads; it is bound to a table once per run, constants are folded, and it is evaluated 1,024 rows at a time into typed arrays with no per-row allocation. A column tested against a constant is still reduced to the ordinary filter, so zone maps and hash and sorted indexes serve it as before, and a group or join key that is an expression is computed into a column./always gives a Float; an empty cell, a non-numeric cell, a zero divisor or an Int overflow gives an empty cell rather than stopping the program, andx % -1is 0. Refused with a message of their own: an aggregate inside an aggregate, an aggregate inGROUP BYorON, aGROUP BYentry that reads no column (group by 1), and anONthat names a table joined later. At 1,000,000 rows (Windows x64,-opt s3, best of three),select price * qtywent from 585 ms to 146 ms andwhere price * qty > 100takes 29 ms (it was a syntax error); a join of 200,000 rows to 1,000 takes 29 ms ont.n = u.wand 62 ms ont.n + 1 = u.w + 1. A differential test holds every form to a row-at-a-time evaluator written from the design's rules, over random tables with NaN, infinities, extreme Ints, empty cells and deleted rows, each also on an indexed twin (9,672 queries over 60 seeds). - Bridges and serving (#1325):
ToCsv,WriteCsv,ToJson,WriteJsonandToMatrix(names[, fill])on bothViewandTable, withTable->FromCsvText,FromJsonandFromMatrix; numbers round-trip exactly throughFloat->ToShortString.TableServer, inquery_server.oblsoquery.obldoes not pull inWeb.HTTP.Server, answersGET /tables,GET /tables/NAMEandGET/POST /queryas CSV, JSON or.otab, refuses anything but aSELECT(403), any query naming a file or URL (403) and any result over its row limit (413).RemoteSourcequeries a published table as a local one, sending its columns, limit and aSetWherecondition so only matching rows cross the network. - Safe for threads (#1378, #1389):
Table->GetErrorandCatalog->GetErroranswer the calling thread's own last error, through two new runtime functions,Thread->GetCurrentId()andThread->IsAlive(id). ACatalogcan be shared: its table map, plan cache and file cache are locked only for a map operation, sixteen file locks give single-flight loading so threads that miss together read a file once, andCREATE TABLEby several threads at once lets exactly one succeed. Writes to a table are still the program's to order.TableServeranswers requests at once: it held one mutex for each request from parsing to the last byte sent, which only serialized reads and never guarded the program's own writes. Each request now holds a read share of a writer-preferring lock,TableServer->BeginWrite()andEndWrite()bracket the program's changes to published tables (DML, indexes, columns,Compact) and keep requests away from them, andPublishandUnpublishno longer lock. Eight clients sending oneGROUP BYover 20,000 rows went from 48 to 112 requests a second (Windows, 24 logical processors); with the program's writes inside that pair, no request reads a table half changed (in the new test, 18 to 136 reads a run saw a change half done before). Writes over the wire were and are refused with 403. - Correctness first (#1291, before the redesign):
AverageandSumcounted string and Nil cells,order bythrough aMapkeyed on the value lost rows with equal values,wherecomparing an int column with a float literal aborted the VM,anddid not bind tighter thanor,order byafterwherewas ignored,LIKEcompiled a regex per row, andDeleteof the only row crashed. All fixed, withMin,MaxandCount(name)added.
AI foundation
The first steps of docs/AI_FOUNDATION_DESIGN.md (#1451): use Hugging Face models from Objeck the way Python does, with no Python installed.
TextClassifier(System.NLP.Onnx, #1456): opens a Hugging Face model folder (whatsave_pretrainedwrites, with an ONNX export beside it) and classifies text aspipeline("text-classification")does: the tokenizer fromtokenizer.json(orvocab.txtandtokenizer_config.json), labels fromconfig.json'sid2label, sigmoid or softmax scoring astransformerschooses, only the inputs the export declares, and the maximum length the model and tokenizer allow. Batches are padded to their longest text and truncation is reported on eachPrediction. Token ids match Python exactly and scores agree to 1e-4 on three generated models; a real SST-2 model matches Python to six decimals at batch size 1. When DirectML or CoreML cannot open or run an export (both refuse thetorch.export-based exports current PyTorch writes), it runs on the CPU instead.HubClient(the new libraryai_hub,System.AI.Hub, #1463):Snapshot(repo, files[, revision])andDownload(repo, file[, revision])fetch from the Hugging Face Hub asfrom_pretraineddoes. One API call fixes the commit, so every file of a snapshot comes from the same one; each file's size and SHA-256 (or git blob hash) must match what the Hub reports before it is kept, using the new streamingCipher.Hash->SHA256File. The cache (OBJECK_HUB_CACHE, default~/.objeck/hub) useshuggingface_hub's layout;OBJECK_HUB_OFFLINE/HF_HUB_OFFLINEand an unreachable Hub serve from it, Python's own cache is read as a fallback,HF_TOKENgoes to the Hub and never to the file CDN, and names that could escape the cache are refused.BpeTokenizer(System.NLP, #1462): byte-level BPE as RoBERTa and GPT-2 use it, with the GPT-2 pre-split, RoBERTa's special-token layout, longest-first pair truncation,<mask>'s whitespace rule andByteLevelcharacter offsets. It matches thetokenizerslibrary on all 348 parity encodings, and on DistilRoBERTa's own 50,265-token vocabulary.SubwordTokenizeris the interface it shares withWordPieceTokenizer, andTokenEncoding->GetOffsets()gives each token's span of the text.- Downloads of any size (#1459):
HttpClient->GetToFile(url, path),HttpsClient->GetToFile(url, path[, pem]),QuickGetToFileandGetDownloadSize()stream a body through a 256 KB buffer topath + ".part"and move it overpathonly once it is complete, so an interrupted download never leaves a truncated file; any other status leavespathuntouched.Getno longer returns no content for a body over 256 MB, and a chunk over 256 KB is read rather than refused.
Runtime
- Interpreted code no longer has an
Intrewritten by a collection (#1466, #1470, #1478, #1452; design indocs/GC_ROOTS_DESIGN.md): a minor collection treated every operand-stack word that looked like a young object's address as a reference, so anIntthat equalled one changed value. The VM now computes a reference map for each method, and for interpreted frames the collector skips the words the maps prove areIntorFloat; anything not proven keeps the old handling. Typing stops above any frame that compiled code sits under, because a compiled method's pending words are not mapped yet; compiled frames are planned for the next release.OBJECK_GC_ROOTS=conservativerestores the old behavior, andOBJECK_GC_ROOTS=auditchecks the maps against the live stacks at every collection and prints a summary at exit. - An object's instance ID survives the collector moving it (#1472):
GetInstanceID()was the object's address, and a minor collection moves young objects, so an object's ID changed when it was promoted.BasicCompare'sHashIDandCompareare both the ID, so aHashorMapkeyed byBasicCompareobjects lost its entries once the keys moved (with the default nursery and enough allocation, 2,000 of 2,000Hashlookups missed), and anIntholding an ID was an address the collector rewrote wherever it found one (#1452), heapInt[]elements included. The ID is now a number assigned once per object from a counter; the collector moves it with its object and forgets it when the object dies, so a reused address never inherits one. - Three traps no longer leave words on the operand stack (#1458):
UDPSocketServerbind and close andFile->TempNamenever popped the arguments their bytecode declares, so each call left one or two words behind for good, and the collector, which scans the whole operand stack, kept what they named alive (here the socket server itself). - Allocation no longer searches a free list (#1308): the old generation's free-block cache kept one list per power of two holding blocks of any size in that range, so after a major collection freed thousands of small blocks every allocation of a slightly larger size walked the whole list and then allocated fresh anyway. Each list now holds one size class, four per power of two, so a cache hit is one pop. The first read of a 4,000-row text column went from 0.96-1.03 s to 0.023 s, and building 16,000 rows from 2.06 s to 0.20 s (Windows x64). A stack sampler had put every sample in
MemoryManager::GetMemory. - The major-collection threshold follows the live heap (#1311): it started at 8 MB and grew only after eight majors in a row freed nothing, so a program keeping a table or a model past 8 MB ran a full collection on nearly every array allocation. After each major the next threshold is twice the live heap, never below 8 MB. Medians of three runs on Windows x64: binarytrees 18 from 13.9 s and 36 majors to 10.0 s and 10, peak memory 353 MB to 273 MB; a 70,000-row
.otabprobe from 2.97 s to 2.09 s. A given--gc-thresholdis a trigger that scales with reality (#1383): it holds exactly as given while a major collection leaves at least a quarter of it free, so--gc-threshold=64kstill collects about every 64 KB of allocation for a program with little live data, which the nightly stress run relies on; while more than three quarters of it survive, the next major waits until the heap reaches twice the live size; and the given value returns once the live heap fits under it again. Before that, a threshold below the live heap collected on every allocation: a program keeping 4,000 objects under--gc-threshold=64ktook 23,539 major collections and 11-18 s, and now takes 8 and 0.1 s (Windows x64).OBJECK_GC_TRACEreports, once per run, the first time a program outgrows its threshold;docs/cli_options.mdstates the semantics. - Collections (#1341):
Collection.Hashwas a chained table whose buckets wereCompareLists with built-in cursors, about four objects per entry, resizing in fixed steps that stopped at 196,613 buckets, and keys of different classes that collided aborted the VM with an invalid cast. It is now linear probing over parallel key, value and hash arrays, doubling at 70% load with no limit, deleting by backward shift, and comparing keys only when they share a class.Cachefound and reordered its LRU list with a linear search on every call and evicted a different entry when a key it already held was updated; it now keeps aHashof nodes in a doubly linked list. New unboxed classes inCollection:IntVector,FloatVector,IntMap,IntSetandBitSet. Measured on Windows x64 (--stat min, five runs, about one million operations a row):hash_int_insertat 1M 2.506 s to 0.424 s,hash_str_findat 1M 2.056 s to 0.401 s; appending 1M to anIntVector0.058 s against 0.933 s for aVector<IntRef>. On macOS the collector's old-generation pointer set clustered under the nano allocator's power-of-two stride (one major collection took 29.8 s on the hosted runner, every sample inPtrSet::insert); each 64 KB address window now gets a pseudo-random offset, mean probe distance 613,504 to 0.50 in the model,collect_hash_openabout 6 s to 3.7 s on Windows at a cost of about 2% on binarytrees.perf-results/libs/run_lib_bench.pyruns the library benchmarks against a baseline. - Runtime batch (#1304):
Int,Float,ByteandCharget bulkCopy(dest, dest_offset, src, src_offset, count)andFill, native traps 4-14x faster than an element loop that raiseIndexErroron a bad range;Float->ToShortStringgives the shortest text that reads back as the same value andJsonElement->New(Float)writes it, so JSON carries0.5rather than0.500000;"1e400"->ToFloat()gives infinity instead of stopping the VM, and the interpreter and JIT share one parser (the JIT's ignored a signed hex prefix);Serializer->Write(Bool[])over-read 8 bytes per 1-byte element andReadBoolArrayreturned an Int array posing asBool[];Int/Float->ToBytesandByte->ToInts/ToFloatsconvert whole arrays little-endian;Serializer->Serializecompressed at zlib level 9 (5 s for 32 MB) and now uses level 1 (0.23 s, 14% larger), withCompressZlib(input, level)for a choice. - Compression (#1296, #1314, #1350):
"hello"->ToByteArray()->CompressGzip()ended the program, because the buffer was sized for zlib's 6-byte wrapper rather than gzip's 18; the decompressors returned partial output as success at their 512 MB limit and leaked stream state on every error path; the six compression traps now report failure through a recoverable error.Byte->CompressBr/UncompressBrwere documented as Brotli and were raw deflate (zlib withwindowBits -15); they are nowCompressDeflate/UncompressDeflate, in the same trap positions so compiled programs keep running, andWeb.HTTP.Server'sCompression->BR, which sent that raw deflate asContent-Encoding: brto clients that could not read it, is removed.UncompressDeflate(input, limit)andUncompressGzip(input, limit)never grow pastlimit + 1bytes, which is what lets the archive reader refuse a zip bomb after producing at most the size the member claims. - Files and processes (#1297, #1307):
File->Renamereplaces an existing target atomically on Windows (MoveFileExW), as POSIXrenamealways did, so write-beside-then-rename saves work there;File->CreateNew(exclusive create),Runtime->GetProcessIdandRuntime->IsProcessAliveare new, for the.otabwriter lock. - Number parsing refuses bad text instead of throwing (#1310): every throwing
std::sto*in the VM, compiler, debugger and REPL was audited.Runtime->SetProperty("float:string:precision", "x")ended the program at the nextFloat->ToStringwithinternal error: invalid stoll argument; it is now a recoverable error and the property keeps its value. A non-numeric HTTP/2:statusthrew through C frames;"{$f:.99999999999}"was an internal compiler error with no position and is now a compile error; a bad DAPContent-Lengthended obd with an exception;/g 99999999999exited the REPL;--jit=4294967297ran as--jit=1and is now a usage error. - Interpolating a Nil prints
Nil(#1306):"x={$s}"with a NilStringor object stopped the program insideString->Append; a missing value is often what a message is about, so it now readsx=Nil.String->Append(Nil)and+with a Nil operand still raise. - Memory and thread hazards found by the heap verifier and CI (#1388, #1390, #1392, #1393): on POSIX every broken-down time was read from the one static buffer
localtime()andgmtime()share, so two threads callingDate->New(),AddDaysorFile->ModifiedTimeat once read each other's fields (a catalog stamping files from six threads read some twice), and on Windows the function returned the address of a local; each caller now owns its buffer. Objects the traps build (a reflection call'sClass,MethodandDataType, aCommandOutput, the socket an accept returns, a recoveredError) were stored into with a plain C++ store, so when the nursery was full and the holder landed in the old generation nothing recorded its young references and the next minor collection left it pointing at the old copy, an intermittent wrong method name, socket address or error message; the stores now run the write barrier. AFloat[]field read back bySystem.Deserializerwas typedInt[]to the VM. On Linux with transparent huge pages set toalways(the default in WSL and on many distributions) an idle VM thread held up to 2 MB resident, because the kernel backed its stack with a huge page at the first touch; thread stacks and call stacks are now mapped withMADV_NOHUGEPAGEand a guard page, and 100 idle threads cost 7.9 MB rather than 37.0 MB (WSL2, medians of 3). - A conditional's value survives a collection (#1436): a conditional (
c ? a : b) stores each branch's value in the method's untyped and/or slot and loads it again after the join, and the collector treated that slot as a root nowhere: interpreted frames on every platform stepped over it, and so did compiled frames on ARM64. When another thread's collection ran between the store and the load (the interpreter parks for one every 1,024 instructions), the object was promoted and the load read its old nursery address, so a constructor likeLiveNode->New(i, (i % 64 = 0) ? Nil : head)stored a dangling reference; the nightly heap verifier caught it on five tests across Linux, macOS and Windows ARM64. Both walks now mark the slot conservatively and relocate it. The gap predates v2026.10.1; this release's more frequent major collections under--gc-threshold(#1311) made it reproducible.gc_cond_value_threadsfailed on every pre-fix run, andcore_thread_gc_stressran 1,500 times clean on the hosted Windows ARM64 runner with the fix. - An
.obefrom another toolchain version says what to do (#1387): the loader refused it with 'appears to be invalid or compiled with an incompatible version of the tool chain', which named neither version, and a VS Code user whose extension shipped an olderobjeck_lsp.obethan theobrit started saw exactly that. It now names the file and both toolchain versions, says which side is older and to recompile with thisobc(or to upgrade the runtime when the program is the newer);obdshares the loader, and running an.oblas a program says the file is a library and how to use it.check_obe_version_mismatch.pypatches a probe's version one down and one up on every CI leg.
Concurrency and the call stack
- Calls nest 16,384 frames deep, and a full stack is recoverable (#1377):
CALL_STACK_SIZE 256, unchanged since commit c2208e3a0d, ended the shipped Rosetta exampleman_or_boy.obswithcall stack bounds have been exceededand any recursive-descent parser past about 300 levels. Raising the constant alone would have broken what it quietly protected: an operand stack of 768 words that was never bounds-checked, compiled frames on a 1 MB Windows thread stack, and aCallCompiledpath that printed and exited so compiled recursion could not be recovered bytry/onor?->. A per-threadCallStacknow reserves the frame array and operand stack for the maximum and commits on demand, so the array never moves and the JIT's cached frame slots stay valid;--call-depth=<frames>andOBJECK_CALL_DEPTHtake 256 to 1,048,576 with a default of 16,384;PushFramechecks the committed depth and that 1,024 operand words remain; and the compiled call site compares the native stack pointer against a per-thread floor, so running out of native stack is the sameRuntimeError, recoverable compiled or interpreted.obrandobireserve 32 MB for the main thread and VM threads 16 MB on Windows (8 MB on POSIX, scaled only above the default depth, since transparent huge pages made larger thread stacks cost resident memory on Linux). An unhandled error's listing shows the innermost 40 and outermost 10 frames. CLBG benchmarks moved within run-to-run noise (plus or minus 2%); per extra thread at 1,000 idle threads, 93.7 KB to 94.4 KB on Windows. - Blocking I/O parks for the collector (#1300, #1302, #1317, #1319, #1326): the collector is stop-the-world with cooperative safepoints, and a thread blocked in TLS, HTTP/2, HTTP/3, DTLS, UDP, a named pipe, console input,
Runtime->Commandor a DNS lookup held up every collection in the process; when the thing it waited for was another thread of the same VM that needed a collection first, neither moved again. Every such trap now parks, re-rooting what it still needs on the operand stack and copying through C buffers, because the collector can move a parked thread's young objects. Native libraries could not park at all:VMContextgainsbegin_blocking/end_blockingandlib_api.hanAPITools_Parkedscope guard, with the ODBC (SQLConnect,SQLExecute,SQLFetch, ...), SDL (SDL_WaitEvent,SDL_Delay, PCM loops) and OpenCV (VideoCapture,waitKey) waits bracketed by it; a library built against the old header keeps working unparked, and one built against this header runs on an older VM. A TLS reconnect whose new connect failed left a freed context in the instance and was fixed on the way. - TCP and TLS accepted sockets shared one class-id cache (#1320): whichever kind of server accepted first fixed the class of every socket accepted after it, so a process serving HTTP and HTTPS side by side handed a
TCPSecureSocketto the plain server's callers, and a TLS accept that got the four-fieldTCPSocketclass wrote its address and port past the object's end, where the collector never traced the string. An audit of every trap that stores into an instance after allocating found no further hazard: no allocation inside a VM trap can start a collection, and only an explicit park can. - Calls back into Objeck from C++ run on the thread's checked call stack (#1386):
APITools_MethodCall, which a native library'sAPITools_CallMethodandClass->Instancereach, built a fresh interpreter and call stack for every call, so none of the bounds of #1377 applied, and a recursion through a callback (native code calling an Objeck method that calls it again, or a constructor making another of its class by name) ended in 0xC00000FD with no output on Windows, even insidetry. A callback now shares the thread's call stack and operand stack, checks the native stack at each level (about 7.5 KB a level, compiled), and under aTry()beneath the native call records the error and raises it when the native function returns; a thread the VM did not start has no call stack to share and keeps its own.
Networking
- A connection reset mid-reply is a failed request (#1469): on Windows a server's reset can discard the rest of a reply already in flight, and
HttpClientread what was left as a valid response: headers cut after the status line gaveGetToFilea 200 with an empty file (andGeta 200 with no content), no status line gave a code-0 response, and a socket already closed raised aNilError. Each now returnsNil. - A TLS read times out on a peer that goes quiet (#1471): a plain TCP client socket starts with a 30 second receive timeout, but a TLS client socket started with none, so a peer that stopped sending without closing blocked
TCPSecureSocket,HttpsClientand the HTTP/2 client for good, in the handshake or after it. TLS client sockets now start with 300 seconds, longer than plain TCP's becauseAPI.OpenAIandAPI.Googlewait on non-streaming requests while a model reasons;OBJECK_TLS_RECV_TIMEOUT(milliseconds,0for none) sets it. - A redirect to another host no longer receives your credentials (security, #1454):
HttpClientandHttpsClientre-sent every header the caller had set, and the whole cookie jar, on every redirect hop, so a 3xx to another host or port received the caller'sAuthorization,Proxy-Authorizationand cookies;API.OpenAIandAPI.Googlesend their bearer tokens throughHttpsClient. Once a chain leaves the original scheme, host and port, those three are dropped for the rest of it, as curl and Pythonrequestsdo. Same-origin redirects keep everything, and neither client follows a redirect to a different scheme. - DTLS works for the first time (#1318, #1333, #1376): the parser accepted all 18
SOCK_DTLS_*directives and the emitter had a case for none of them and no default, so everyDTLSSocket/DTLSSocketServermethod compiled to nothing and returned whatever was on the operand stack; on WindowsListencrashed the VM. With the cases emitted (and adefaultthat stops the build on any directive the emitter forgets), running the handlers found that accept never completed a handshake (the cookie's transport id was never set),ReadBufferwaited to fill a buffer a datagram never fills, a second client was never served, and the server listened on one address family chosen by the platform.DTLSSocketServer->New(cert, key, passwd, address, port)binds one address and keeps to it across the re-bind each accept performs, and a write after the peer has gone fails with the peer'sclose_notifyor the OS reason rather than returning true forever. - Servers take IPv4 and IPv6 clients on every platform, or one address (#1322, #1326, #1373): on Windows an HTTPS server refused every connection to 127.0.0.1 while its HTTP sibling refused every connection to ::1, and on Linux both were IPv4 only. Every wildcard server now binds
[::]withIPV6_V6ONLYoff, falling back to0.0.0.0on a machine with no IPv6 stack, and reports an IPv4 client asa.b.c.d.TCPSocketServer->New(address, port),TCPSecureSocketServer->New(cert, key, passwd, address, port),UDPSocketServer->New(address, port)andWebServer->Serve(callback, address, port)bind one address, with"localhost"always 127.0.0.1; a bad address leavesListen()false with the reason inGetLastError().UDPSocket->Newresolves names and IPv6 throughgetaddrinfo, tries each address its name resolves to and moves on when one is refused, and a Windows UDP server no longer fails its next read because a reply to a departed client drew an ICMP port-unreachable. - A server bound to port 0 reports its port (#1372, #1380):
TCPSocketServer,TCPSecureSocketServer,DTLSSocketServerandUDPSocketServergainGetPort(),WebServer->GetBoundPort()is the real port onceServeis listening, andMcpServer->Serveaccepts an"http_port"of 0 withMcpServer->GetBoundPort()reporting it. Thirty-two regression tests that listened on fixed or randomly picked ports, wrote fixture files of fixed names or shared the system temporary folder now use port 0, per-process names and private folders; before, two suites on one machine collided in 14 of 31 server tests on both Windows and Linux. - Every socket failure says why (#1348, #1349, #1371, #1373): a
TCPSocketServeron a taken port reportedGetLastError()as "The operation completed successfully." on Windows, because the error was read whenGetLastErrorran rather than when the bind failed and a successfulclosesocket()had reset it; worse,SO_REUSEADDRlet a second Windows server bind and listen on a port a server already held, so aWebServeron a taken port came up and served nothing.TCPSecureSocket->GetLastError()always returned Nil, so a refused certificate, an absent server and a missing key file looked the same. Every plain, TLS and DTLS call now records its own outcome per thread as it happens,GetLastError()is Nil when the thread's latest call worked, a connect that fails at every address reports the most informative reason (an answer from the network beats a route that never left the host), a verification failure lists mbedTLS's reasons ("The certificate is not correctly signed by the trusted CA"), andTCPSecureSocket->WriteByte/DTLSSocket->WriteBytereturn whether the byte was written rather than always true. A write to a peer that hung up fails with "Broken pipe" instead of killing the process, becauseSIGPIPEis ignored from VM start on POSIX;Winsockis started once for every socket, so in-process VMs (obi, embedding hosts) can open plain TCP sockets on Windows. A Windows connect timeout waited only on the write set while Winsock reports a failed connect in the except set, so a refused connect sat out the whole timeout. - TLS verification, said once and done the same everywhere (#1323, #1335, #1349): on Linux only, a client connecting by IP address refused a certificate that named that address in an iPAddress SAN, because the distribution's mbedTLS 2.28 compares an address only against DNS names and the CN; a verify callback now reads the iPAddress entries from the raw extensions and clears only the name-mismatch flag, only when the address is listed, and does nothing with mbedTLS 3.5 or later.
TCPSecureSocketServer->GetIssuer/GetSubjectread the server's configuration as if it were a session and returned Nil; a server now reports its own certificate and a socket its peer's.DTLSSocket->New(..., verify := false)was documented as allowing self-signed certificates and never did; the doc now says what it does (verification unless the process is in insecure test mode, whichverify=trueoverrides), andTCPSecureSocket->SetVerifyPeerno longer changes the mode of a connection that was already verified. - HTTP (#1300, #1328, #1340):
Web.HTTP.Serverwrote a handler's headers and body only for 200, a few codes went out bare, and every other code was sent as 400, so a handler could not send 201, 204, 301, 304, 401 or 403 andSetCodeContent(404, "...")lost its body; bothProcessResponsecopies now share one formatter for any code from 100 to 599, with the RFC 9110 reason phrase and no body on 1xx, 204 and 304, and the clients read no body on those codes rather than waiting out a 30 s timeout.Url->Encodepassed non-ASCII text through raw (an invalid URL) andDecodeturnedcaf%C3%A9intocafé; both are now UTF-8, withEncodePath/DecodePathkeeping/and a literal+. Query parameters split at the first=, soeq=x=yand a bareflagare kept, through a publicRequest->ParseQuery.
Windows and Unicode
- Non-ASCII text everywhere on Windows (#1313, #1327, #1357, #1362):
données.txtwas created on disk asdonnées.txt, and a file another program had nameddonnées.txtcould not be found, because every file and directory trap narrowed the name to UTF-8 and passed it to an ANSI API that read it in the active code page; the narrow calls agreed with each other, so a program could read back the names it had garbled itself. Every file, directory, link and pipe trap now goes through the W APIs (also fixingFile->Sizetruncating above 2 GB, a 256-character full-path buffer and four Nil dereferences). Command-line arguments reached obr, obc, obd and obi through the ANSI code page and were decoded as UTF-8, so a source,.obeor library path with a character outside the code page arrived as?; each tool now enters atwmain, on MSVC and MinGW builds alike, and the files those names reach are opened by their wide names. Environment variables,OBJECK_LIB_PATH,--lib-path, the native-library loader (LoadLibraryA, which loaded nothing from an install directory with a non-ASCII name), the CA bundle and certificate loads, andCommandOutput's temp file (unquoted, so a temp directory with a space sent the output elsewhere) are all UTF-16 now. On POSIX too, an argument was widened byte by byte through a signed char, so a program never received a non-ASCII argument intact.obu,obband theobnlauncher take UTF-16 arguments (obu updateaborted in such an install directory becausefs::path::string()throws), andobnalso gained argument quoting, a PATH buffer longer than 1 KB, and a change to its own directory.obd's interactive mode puts its streams in_O_U8TEXTso the first non-ASCII character no longer leaveswcoutfailed, its command scanner accepts non-ASCII letters in a file name, the launcher'sSetObjEnv.cmdswitches code page around the lines that hold paths, andobuwrites console text throughWriteConsoleW. - Found on the way:
lang.obsis compiled by the_SYSTEMbootstrap compiler, which skipped the all-paths-return check for the whole library, and three methods shipped with a return type and no return:Runtime->CommandOutput(command)crashed its caller on Windows on every call, andAlarm->GetInterval()andUDPSocketServer->Bind()returned whatever the trap left. The bootstrap compiler now checks every method except one whose body is a trap (#1327).CommandOutput(command, variables)kept a variable's old value when it was already set and left an unset one set to""afterwards, on every platform.
Compiler and language
- A library class keeps its interfaces without the program's
use(#1465): a library recordedimplements ScoreMetricas written, and the linker resolved that name against the program'suselist, so in a program withoutuse System.MLa cast ofAucMetrictoSystem.ML.ScoreMetricfailed to compile, and a library's own upcast to an interface the program had notused failed at run time ("Invalid object cast"). Libraries now record the fully qualified names they resolved, and the linker also looks in a class's own bundle, so libraries built earlier resolve too. The 13 libraries that name an interface unqualified are rebuilt. - A quote inside
{$...}is a compile error, not a hang (#1439):"x: {$(String->Join(items, ", "))}"never finished compiling;obcgrew until it was killed, near 16 GB. The scanner ends a string at its first unescaped quote, and the statement list then parsed the same unusable token again and again without consuming it. A statement that consumes nothing now steps past its token, and a string that ends inside an open{$is reported as such, with the fix: compute the value into a local first. - A bounded generic returns its type argument; literals in
{$...}(#1445): a program's ownHolder<T : Compare>was rewritten in place to its bound when the class was analyzed, so a caller analyzed afterwards sawGet() ~ Compare:Holder<String>->Get()->Size()was undefined andSet()accepted anyCompare. The type now keeps the parameter's name, and the call maps it to the type argument. A literal directly in an interpolation ("{$0x1F}","{$42}","{$'a'}","{$true}") was looked up as a variable, because no name starts with a digit or quote andtrue/falseare reserved; it is now parsed as an expression, as"{$(0x1F)}"and"{$255:x}"already were. - Numeric literals (#1332, #1345, #1355): a float literal outside the double range compiled silently (
1.0e999as infinity,2.4e-324as0.0) and is nowFloat literal out of range, judged by the value rather thanerrnobecause glibc and MSVC disagree on subnormals;-9223372036854775808was rejected and is accepted as the operand of a unary minus, Java's rule; a negated out-of-range integer reported "Expected expression"; one bad number produced a run of follow-on errors (eight inside an array literal) and now produces one. A decimal-looking literal with a leading zero was read as octal with no diagnostic (010was 8,0_10was 8,0bwas 0):0o17is octal, with_separators and theusuffix, and010or09is one error that suggests0o10or10. The debugger's expression scanner follows the same rules (it saturated integers, printed Int's minimum off by one, and scanned no exponent). - Name resolution (#1312, #1324, #1336, #1343): with two
used bundles that both declareclass Foo, an unqualifiedFoosilently resolved to whichever bundle the search reached first, and a program's class silently beat a used library's of the same name; it is nowAmbiguous name 'Shape': 'A.Shape' (x.obs:13) and 'B.Shape' (x.obs:21) are each in scope through 'use', while a qualified name, the current bundle's own name and the class's own enum still win. The check found that a callee's parameter and return types were resolved in the caller's bundle. Bundle-level enums and aliases are named with their bundle, as classes are (Query.Structured.ColumnTypeis the enum's real name), so two bundles can each declareenum Kindand a program'senum ColumnTypeno longer collides withquery.obl's; this is the library-format change. Constructors were inherited by accident:Kid->New()with noNewof its own built the parent, and a class with no constructor built aSystem.Basethat failed at the first cast. A class's enum is in scope in its own class, its children and the other classes of its bundle, and must be qualified from elsewhere.x->As(Kid)->Only() <> "only"failed with an invalid-cast error,Defs.ColumnType->INTdid not resolve, and two classes each declaringenum Typeresolved to the first. - Arrays (#1299, #1309, #1339):
["a", "b", v]crashed the program at every-optlevel (a string-pair counter never reset, so the second literal emitted aSWAP_INTwith one value on the stack); a call returning an interface made on an array element failed a cast at run time because the result's cast was emitted before the call; a literalNildid not bind to an array parameter;Vector<String[]>is a clear error;[["a","b","c"],["d","e","f"]]had its dimensions swapped (3 x 2 holding a 2 x 3 layout), a 2-DCharliteral reported a first extent of 420913348705 because the VM's copy assumed a 1-D header, and 3-D literals were refused. A call on a whole array (a->Fill(7),block->CompressZlib(1)) pushed the array after its arguments, so every multi-argument call on an array got its arguments in the wrong slots andCompressZlib(1)passed1as the array;names->ToString()on an object array boundSystem.Base's instance method to a static call and the three*ArrayRefclasses recursed until the stack overflowed. - Calls and loops (#1299, #1346, #1347, #1350):
obcsegfaulted on anyforwhose condition was not a<comparison (for(i := 1001; true; i += 1;)), because range detection cast the condition to a binary expression unconditionally; an instance method could not call, with no receiver, an instance method inherited from a library class (GetName()in aThreadsubclass'sRun()); named arguments on a library method with several defaults were "ambiguous"; a chained call on a library generic class lost its type arguments; a ternary of an enum constant and a call chain failed to type-check;Int->F(..);on its own line was "Invalid statement". The label merging at-opt s2/s3could give a merged label the id of another label in a linked library method, so adding classes toquery.oblmade every query loop forever; fixed with a shared maximum-label scan that also coversTRY_START. - Narrowing is checked (#1299): a
String[]passed forCompare[]compiled and then failed at run time, but only for array literals; one rule now applies to literals, variables and call results. Widening to a parent or interface array needs no cast, and implicit narrowing (Base[]forKid[]) is a compile error, since nothing could check it at run time; an explicitAs(Kid[])still works. - A Float or function-reference conditional meets at the join as what it is (#1397):
c ? a : bleaves its value in a local at the join and stored it with the Int opcodes whatever its type. A Float conditional was right in the interpreter, but the JIT will not store a float register that way, so the method ran interpreted without saying so (a loop over a million elements with a?:on floats: about 113 ms a pass, 2.7 ms once compiled); a function reference is two words and the store took one, so a compiled method called through the closure of the branch it compiled last, giving wrong answers or an access violation. Found measuring SQL expression evaluation. No new opcodes. - A subscript on a later link of a call chain (
v->Get(r)->Get()[c]) no longer crashes the compiler (#1426).
JIT
- A dead object no longer outlives a stale spill slot (#1468): an AMD64 method's entry zeroed nine of the ten spill slots the collector scans, so a method entered at the same frame pointer as one that had spilled a reference there kept that object alive for as long as it ran (a dead 32 MB array outlived every collection). Entry zeroes all ten; ARM64 already did.
--jit=offruns the interpreter only (#1455): a method declarednative(every library method so declared, too) was compiled on its first call whatever the setting, so--jit=offandOBJECK_JIT_DISABLE=1still ran compiled code, and the nightly differential's interpreter column was not one.- A compiled Float division by NaN no longer stops the program (#1399): the x64 guard for a zero divisor compared with
ucomisdand jumped on the zero flag, which an unordered compare sets as well as an equal one, sox / NaNraisedDivide by zerowhere the interpreter computes NaN. It surfaced once #1397 let methods with a Float?:compile. ARM64 compares withfcmp, which leaves the flag clear, and was right. Float->NaN(),Float->Inf()andFloat->NegInf()compile (#1423): neither backend knew the three opcodes, and one unsupported opcode returns the whole method to the interpreter.$Float:Inf()is a one-opcode method that the-opt s3inliner pastes into its callers, so every method that wrotebest := Float->Inf()ran interpreted, among themSystem.ML.KDTree:Nearestand the searches and min/max scans inml_neighbors,ml_cluster,ml_tree,ml_bayes,ai_search,ai_game,ai_optimizeandmath. Answers were right; onlyOBJECK_JIT_REPORT=1could see it. The three are float literals now, from the same expressions as the interpreter's handlers, so a compiled method gets the same bits, including the same quiet NaN.- A multi-dimensional array access gives back its index register (#1424):
a[i, j]folds its indices into the first one's register, and a later index that was already in a register (computed, held in the local-register cache, or the copy of a pinned loop local) was added in and then dropped without being returned to the pool. After eight such accessesGetRegisterfound the pool empty with nothing live on the working stack, and the method went back to the interpreter, 10 to 20 times slower; the generated code was always correct. Both backends had it.EllipticEnvelope's distances went from 1.68 s to 0.14 s, and the BERT classifier head's Adam step now compiles. A failed compile now reports the failing instruction, its opcode, the inlined callee and the reason (no free integer register: the working stack holds 0 integer and 1 float register(s)...), andOBJECK_JIT_REPORT=1audits the register pool at every label. - A loop's collection safepoint keeps the values pending across it (#1425): the JIT polls for a stop-the-world collection at every loop header and parks the thread with a plain call to
MemoryManager::SafePoint, on the assumption that the working stack is empty there. When the-opt s3inliner pasted a copy loop into an expression after an earlier operand had been evaluated, that operand was still pending in a caller-saved register, which the call clobbers, and a young object it points to may be moved by another thread's collection while the thread is parked; the next use was a Nil dereference or a segmentation fault in compiled code. Only a multithreaded program whose loop polled during another thread's collection reached it, which is why it was rare (10 times more frequent with a small nursery) and surfaced as an intermittent failure of the new SQL-expression concurrency test. The slow path now spills every register the working stack holds into scratch slots the collector marks and relocates, reloads them after the park, and leaves the method to the interpreter if there are more values than slots; the fast path is unchanged. AMD64 and ARM64.
Libraries
System.NLPworks on real text (#1286; design indocs/NLP_DESIGN.md): sentiment scored 4 of 10 ordinary sentences ("not good" was positive), the "Porter" stemmer was five suffix rules (runningtorunn), the tokenizer split on the space character only, the sentence splitter broke onDr.and3.50, a TF-IDF refit kept the old vocabulary, andNormalizedEditDistancecrashed on Nil. The stemmer is now Porter (1980) as Porter's referenceporter.c, agreeing with NLTK on 8,559 of 8,559 words; the tokenizer handles contractions, decimals, hyphens, Unicode and all whitespace; sentiment is VADER-style (negation, boosters, capitals,!,but, a ~600-word lexicon,LoadLexiconfor VADER's own file) and on Pang and Lee's 10,662-sentence corpus agrees with NLTK's VADER on 95.5% of labels when given VADER's lexicon; TF-IDF uses smoothed IDF and L2 normalization by default.Data.JSON,Data.CSVandQuery.RegEx(#1288, #1289, #1290): a JSON number with an exponent (-1.2e-05, which embeddings routinely contain) made the whole document fail to parse, so one such value lost an entire LLM response; the lexer follows RFC 8259 and the invalid forms it used to accept (01,1.,.5,1e) are errors. A quoted CSV field containing a newline left the whole table unparsed, a short row failed without saying which, and rows longer than the header were accepted silently; the reader is single-pass RFC 4180,GetError()andGetErrorRow()name the row,New(data, Char)takes a delimiter, andCsvRow->ToString()andCsvTable->FormatRowwrite records that read back. Regular expressions gained real character classes ([a-zA-Z],[^abc],[.,;]and\d \w \sinside brackets were all rejected before),\bas a word boundary (it compiled to a backspace), empty matches forx*andx?, and capture groups (Result->GroupCount,GetGroup(i),GetGroupStart(i);(?:...)does not capture).Data.JSONdecodes strings on parse and escapes once on output (#1403): the parser kept a string's escape sequences as written, soGetValue()of the string"x\"y"returnedx\"y, and parsing then serializing doubled every escape ({"a":"x\"y"}came back as{"a":"x\\\"y"}) and wrote/as\/. Every consumer had to know to callJsonElement->Decode; some did (the MCP handler's ids, the language server, the Ollama, Gemini and OpenAI error and text fields,Query.Structured's JSON cells) and some did not (Response->GetText(), inference errors, the ONNX tokenizer's vocabulary), and the stream parser appended every escape letter twice and ended a string at an escaped quote.GetValue(),GetString()andGetNames()now return the text itself (\" \\ \/ \b \f \n \r \t \uXXXX, a surrogate pair as one code point), an unpaired surrogate escape becomes U+FFFD rather than an error (so one damaged character does not lose a whole model response), an invalid escape (\x, a short\u) or an unterminated string is a parse error naming the offset,/is no longer escaped, and non-ASCII text is still written as\uescapes so output stays plain ASCII.Decodeis deprecated and unchanged, and every internal caller is removed; JSON that was pasted together unescaped (CsvRow->ToJson,KMeansGrouping->ToString, JSON-RPC method names, Ollama upload names, the OpenAI realtime voice) is escaped,Data.JSON.RPCechoes a string id as sent, and the code-doc tool's hand-rolled escaper usesEncode. A round trip of 100,000 random strings is the identity. Parsing and serializing a 100,000-object document took 2.66 s and 2.79 s against 2.89 s and 3.67 s. Found on the way: the language server's formatter scanner never tested for the end of the text and spun at full CPU on an unterminated string, character or#~comment, a#comment on the last line with no newline, or valid code with a literal ending in an escaped backslash and no later quote.- Kernel SVMs (
System.ML, #1426; design indocs/ML_KERNELS_DESIGN.md):KernelSVM, a C-SVC with linear, RBF, polynomial and sigmoid kernels on LIBSVM's SMO solver (WSS3 working-set selection, shrinking, an LRU kernel cache sized in MB) with scikit-learn'sSVCparameters (C,gamma'scale'/'auto'/value,degree,coef0,class_weight); andOneClassSVM(nu, scikit-learn's sign conventions,IsOutlier) on the same solver. Checked against an exact KKT enumeration and an independent SMO on 488 problems. At the default toleranceOneClassSVMcan flag more than thenufraction of its training rows; the documentation says so. - Anomaly detection (
System.ML, #1426; design indocs/ML_OUTLIERS_DESIGN.md): anOutlierDetectorinterface (true is an outlier, a higher score is more anomalous) withZScoreDetector,IsolationForest,LocalOutlierFactor(novelty mode),EllipticEnvelope(FastMCD),DbscanDetector,AutoencoderDetector(on the new multi-layerDenseNetwork: any depth, ReLU/sigmoid/tanh/linear, minibatch Adam) andOneClassSvmDetector;ConfusionMatrix,Metrics->FalsePositiveRate, andSensitivitySweep/SweepResultto sweep any detector's sensitivity. Example:programs/examples/ml/anomaly_sweep.obs. - Forests for labs (
System.ML, #1426):DecisionTreeClassifierandRandomForestClassifiergainmin_samples_leaf, unlimited depth,GetFeatureImportances,Apply, depth and leaf counts and fit times; the split search is one sorted sweep per feature instead of a quadratic one (a depth-12 tree on 2,000 x 41 rows: 52 s to 0.04 s) with bit-identical splits. Examples:svm_lab.obsandrf_lab.obs, on NSL-KDD or a synthetic stand-in. - BERT embeddings (
System.NLP, #1426; design indocs/ML_BERT_DESIGN.md):WordPieceTokenizer(Hugging FaceBertTokenizerparity on 39,154 cases, Unicode tables generated fromunicodedata),ApiSequenceTokenizerfor API-call traces andTokenEncoding; the new librarynlp_onnx.oblwithSystem.NLP.Onnx.BertEmbedder(CLS, MEAN or POOLER sentence embeddings from a BERT-family ONNX export, with a CPU fallback when the default provider cannot open the model); andSystem.ML.EmbeddingClassifier, a trainable binary head over fixed-size embeddings (ReLU hidden layers, class-weighted binary cross-entropy, Adam or SGD, per-epoch loss history). Example:programs/examples/ml/bert_malware_lab.obs, a frozen-encoder malware lab. System.ML.Lab(the new libraryml_lab.obl, #1426; design indocs/ML_LAB_SUPPORT_DESIGN.md):Partition->TrainTest(stratified, seeded, exact),Resample->ToPositiveRatiofor class-imbalance runs against a fixed test set,Stopwatch,Labels,Experimentover any model through aTrial, aResultTablewith text, Markdown, CSV and JSON output, andChartwriting standalone SVG line, bar and loss-curve charts. Example:programs/examples/ml/lab_sweeps.obs.-lib @ainow includesml_labandjson, and-lib @visionincludesnlp_onnxand links again (it lackedmodels.oblsinceonnx.oblbegan linking it).System.Time.Timermeasures wall-clock time on Linux and macOS (#1426); it was process CPU time there, so a timer around a sleep or a blocked thread read nearly zero.McpServer(#1394, #1396): a Nil, incomplete or ill-typed configuration, and any request body that was JSON but not an object, an object withoutmethod, or aresources/readwithoutparams, dereferenced Nil, and the VM ends the process on that, so one request from any client killed the server.Servenow validateshttp_port(0 to 65,535; 0 asks the system for a port),http_session_timeout,debug,name,versionandinstructionsand refuses a bad configuration on standard error naming the key, withMcpServer->GetLastError()giving the reason;McpRequestHandleranswers a malformed request with the JSON-RPC 2.0 error the MCP specification mandates (-32700 parse error, -32600 invalid request including a body over 4 MiB, -32601 method not found, -32602 invalid params, -32603 internal error for a tool callback that raises, dereferences Nil or returns Nil), echoes a string or large id as sent, and keeps its thread.WebServer->Serve(filename)was an empty stub that loaded the file and served nothing; it now says on standard error that it is not implemented.System.ML(#1287, #1341):Matrix2D->Softmaxnormalized over the whole matrix, so with one sample per row each row summed to about 1/rows; it now normalizes each row, subtracting the row's maximum soSoftmax([[1000,1001]])stays finite, withSoftmaxColumnsfor the other axis.Tanhreturned NaN past about 710,Sigmoidcould overflowExp,RandomNormalcould takeLog(0), andStandardScalerturned a constant column into -1s through a rounding-step deviation near 1e-17.KMeans->Groupkeeps its labels' order.- LLM clients (#1292, #1293, #1340):
API.OpenAIandAPI.Google.Geminihad no regression test that gates CI, so each response parser is now a public function testable offline against recorded bodies, which found thatResponse->GetText()returned Nil for every reasoning-model reply (a reasoning item precedes the message), four Nil dereferences (function-call outputs, refusals,b64_jsonimages, which gpt-image-1 always returns),DALLE3_1024_1792sending1792x1024, and every Batch call going to/batchrather than/batches; in Gemini a blocked prompt crashed, API errors were printed but never recorded, andGenerationConfigwas an empty class.API.Ollamais brought up to Ollama's current REST API: tool calling (Reply->GetToolCalls,Chat->AddToolResult), structured output from a JSON schema,/api/embedwith batches,think,keep_alive, the full option set, reply metadata,/api/psand/api/version; chat history was escaped twice on the way back to the server, chat images were sent where the API ignores them, and/api/showand/api/psreplies from current servers crashed. Gemini corpus query results were run throughUrl->Decodealthough the response is JSON, and 22 error sites never unescapederror/message. - SDL2 (#1294): game controllers worked on no platform, because
GameController->Newcalled a native,sdl_gamecontroller_open, that did not exist; a checker now confirms all 425 natives the Objeck side calls exist. Window, controller, drop-file, touch, text-editing and user events can be read; blend modes, batch drawing from oneInt[], float variants andTexture->Update; text measuring, font metrics, styles and wrapped rendering; logical size and scale;Renderer->ReadPixelsandSavePNG; the performance counter andGetTicks64; a queue-modeAudioDevicefor generated audio. - ODBC (#1331, #1337, #1370): running a
ParameterStatementtwice read memory the native library had freed after the first run (0xC0000005 under the JIT, heap corruption without), and bound parameters pointed intoIntRef/FloatRef/Byte[]objects a collection could move before the run; the statement now owns its handle and maps and every value is copied.SetRealbound a double whereSQL_C_FLOATreads a float, andSetBlobbound no length so a blob stopped at its first zero byte. Text was read through one 8 KB buffer and silently cut short, a NULL was converted from that buffer before anything was written to it, and a refusedSELECTfreed its diagnostic with its statement and read as an empty result;ResultSet->IsOpen()andGetBytes(column)are new. A zero-rowUPDATE/DELETEwas reported as -1 on every platform. Every macOS package built bytools/deps/build_macos_deps.shshipped aDatabase.ODBCthat could not load any driver: libiodbc's configure decides whether to prefix entry points with_using a test program that Apple clang 15 refuses to compile, so it answered "yes" and the driver manager looked up_SQLAllocHandleand found nothing. Found the first time the ODBC tests ran on the macOS leg. - Smaller fixes (#1341, #1348, #1352, #1356, #1359):
Byte[]->ToHexString()returned[0x76,0x4f,...]while its callers used it as a digest, andInt[]->ToHexStringgave],];ByteBuffer->ToStringdumped the whole 512-byte backing array,Comparecast its argument toStringand always raised, the firstAppendto a buffer built from an empty array raised, andNew(Byte[])kept the caller's array despite being documented as a copy (Wrapnow does that on purpose);XmlElement->ToStringwrites attributes in insertion order, a parsed self-closing tag serialized to nothing, andGetDecodedText()returns text untrimmed, whichxml:space="preserve"needs. - Library fixes (#1444):
RSSReaderraisedNilErroron theNilresponse of a failed fetch or a missing local file and is now simply not OK (IsOk()is false).AnimatedImageSprite->AddClipscut each frame as high as it was wide.Data.XMLrefused a self-closing root (<a title="x y"/>), soGetRoot()wasNil; an empty document raisedNilError, and input ending in whitespace or mid-name raised an index error; all are now ordinary parse failures, and[n]or[attribute]below a leaf element no longer raises.FindElementswith[n]returned the first n matches and now returns the n-th, as XPath does (1-based, per parent).Query.Structuredread an unknown column in a single-table select list orORDER BYas empty; it is now refused as it already was in joins,WHERE,GROUP BY,HAVINGand aggregates.
Tooling
- Debugger (#1345, #1355, #1360, #1377, #1341, #1404): a DAP
evaluatethat failed answeredsuccess: truewith<error>and printed the reason into the program's output; it now answerssuccess: falsewith the reason asmessage, the same text the console'spprints.stackTracehonoursstartFrame/levelsand reportstotalFrames, since a 16,384-frame stack is no longer sent whole.printand DAP expansion know the open-addressedHashand the unboxed collections.obdsends every output event of a run before theterminatedthat ends it: the adapter forwarded stdout and stderr from two reader threads and sentterminatedthe moment the run returned, so the program's last lines could still be in a pipe and a client that stops listening atterminatedlost them (the macOS DAP flake).terminatednow waits, for at most two seconds, until both readers have reached end-of-file. - Edits are analyzed once, about twice as fast (#1437, part of #1236): every edit parsed and analyzed the program twice, once for diagnostics and again when hover or completion rebuilt the last clean program the diagnostics had already analyzed. An edit now retires analyzed programs instead of freeing them, and a request whose file texts and library list match a retired program exactly reuses it. On the language server's own 8,800-line workspace the native work per edit went from about 1.3-1.5 s to 0.6 s and hover from 1.6-1.9 s to 0.8-0.9 s; a six-line file from 130 ms to 60 ms.
OBJECK_LSP_NO_CACHE=1still turns all reuse off. - Language server (#1330, #1342): the editor API index held no enums at all, so hover on
Query.Structured.ColumnTypeshowed nothing and completion afterColumnType->offered keywords; it now carries every documented library enum and consts set (90 enums, 1,012 items) under the names the compiler gives them, hover shows an enum's items and an item's value, and the analyzer completes items of a program's own enums, including in a class with no fields. The 19 undocumented library enums are documented, nested enums getKlass->Namepages in the HTML reference, and an enum in a private class is no longer listed. The formatter no longer hangs the server (seeData.JSONabove). - Playground (#1363): libraries are inferred from a program's
uselines and qualified names, with each library's own dependencies added (use Query.Structured;links eight libraries, whichobcdoes not chase), a bundle the sandbox cannot offer is refused before a container starts with a one-line reason, and compile errors come back as line/column diagnostics with a hint naming the bundle touse. 42 demos in 10 categories, eight new for typed errors, named arguments, generic methods,ref, jagged arrays, the unboxed collections, SQL and indexes; a rebuilt UI with light and dark modes, a searchable examples menu, clickable errors and a plain text area if the editor CDN is unreachable. The library map is generated fromlib_srcand a test fails when it is stale. - 32 new shipped examples, in learning order (#1440): the release's
examples/folder held 29 programs numbered in the order they were written. It now holds 32 numbered in the order to read them: the language (0-13), data (14-21), networking (22-24), AI and machine learning (25-30) and an SDL2 game (31). Each opens with its compile and run lines and the output it prints, explains its topic for someone new to Objeck, and is commented throughout; between them they cover named arguments,refparameters, generic methods, jagged arrays, the unboxed collections, typed errors, SQL over tables and files, anomaly detection, NLP, a web server, and a game with a state machine and a saved best score. Every one was compiled with the exact line in its own header and run, CI compiles and runs 13 of them with those lines, andwelcome.obsprints the index. - Examples and docs (#1283, #1344, #1361, #1279, #1285): 86 of the 281 example programs no longer compiled, written against bundles and classes long renamed (
HTTP,XML,RegEx,StringMap,IntVector,Console->ReadString); 85 are ported and the three the compiler itself refused led to #1346 and #1347.errors_6.obsships as thetry/on/finallyexample. The getting-started guide covers typed errors, named arguments,ref, generic methods, jagged arrays, the unboxed collections and0oliterals. A one-page architecture diagram,docs/architecture.mdbrought up to date (it still said Objeck had notry), and about 55 MB of third-party manuals, plus unreferenced images and binaries, removed fromdocs/. Design documents precede the next library work: SQL expressions (#1395), anomaly detection (#1400), kernel SVMs (#1401),System.ML.Lab(#1402) and BERT embeddings (#1408).
CI and release
- Release builds (#1447, #1460): Verify Platforms collects the OpenCV license texts the Windows x64 deploy requires, and the macOS legs no longer restore Homebrew's
Backup/from the cache, which madebrew installfail with "same file". - ODBC is tested on every platform (#1370): the five regression tests that open a SQLite data source ran only on the two Linux legs and printed
SKIPon windows-x64, windows-arm64 and macos-arm64, so the native library had no coverage where it ships as a DLL or dylib. Each leg now installs a checksummed SQLite ODBC driver (the Windows ARM64 one compiled from source, since upstream ships none) and registers a per-user DSN, andOBJECK_REQUIRE_ODBC=1turns the skip into a failure. The first macOS run found the libiodbc defect above. - The nightly hardening run is green again (#1434, #1436): its first run since September reported 85 new failure signatures. The heap verifier's hits were the conditional-slot defect above; the rest were the harness. Three tests that run their own program as a child could not find their
.obeunder the differential runner (it now passesOBJECK_TEST_OBE), a million-deep tail recursion needs-opt s3by design, and four tests printed timings and counts to stdout, which the differential comparison requires to match (they now go to stderr). - The Windows regression runner stops a hung test by default (#1427):
run_regression.cmdhad no per-test limit unlessTEST_TIMEOUTwas set, because the only way to enforce one started PowerShell for every test (about 0.3 s each, over four minutes a suite). One watchdog now runs beside the suite and kills any test of its own past the limit (60 s, as on POSIX), so a hang reports "timed out" instead of holding a CI job until the job's own timeout. - Collision-safe regression suites (#1372): two suites on one machine (an agent's run beside another's, or two CI jobs on one runner) collided on fixed ports, fixed fixture names and the shared temporary folder; with every server test on port 0 and every file per process, two whole suites running at once in two trees pass 674 of 674 on both sides.
TESTS.mdis regenerated on master, not in every pull request (#1305): every PR that added a test committed the manifest, so each merge conflicted every other open PR, about ten times in one day. A workflow regenerates it after a push to master and commits only on change.- Debian packages for Ubuntu 24.04 (#1281) are built from the published tarballs, install-tested in a clean container on native x64 and ARM64 runners, uploaded and added to the signed
SHA256SUMSbyrelease-deb.ymlafter every publish; first published beside v2026.10.1. - Smaller (#1282, #1369, #1374, #1381, #1368, #1406):
obc --versionreadsCopyright (c) 2007-2026; the HTTP/3 downgrade test checks at run time that its negative host does not advertiseh3(example.com started to); two independently green PRs did not build together and were repaired at once; a windows-arm64-only test failure was the console host wrapping a double-width character at cell 120, not the code under test;core/compiler/web, which nothing built, is removed;tls_write_byte_resultwaits for the server to hang up before expecting a write to fail, after a Windows ARM64 run in which all 300 writes succeeded because nothing had waited for the peer to close.
Known issues
- A compiled
-opt s3method can keep a dead object alive until it returns: when the compiler inlines a call whose result was spilled across another call, the spill slot is not cleared after the value is reloaded, and the collector scans it, so the object stays reachable while that frame runs. Nothing is freed while still in use. The fix is GC roots phase 3 (docs/GC_ROOTS_DESIGN.md), planned for the next release. - On Windows, a burst of local connections can stall one for about 21 seconds (#1443, #1461): when dozens of processes connect to one loopback server at once, a connect occasionally never completes and fails after Windows' SYN retries run out (1 to 2 runs in 20 of a 60-process test, #1443). Not specific to Objeck: the same loss reproduces between plain Python sockets, and a packet capture of that run shows the Windows stack dropping the client's closing packet (FIN) with the reason
INET: invalid ACK in SYN_SENT state. The server's end then stays open, and a later connect that reuses the client's port is not answered. Why the stack drops it is not yet known. The listen backlog is larger than the number of clients, and the connection-reset fix in this release does not change the rate. A second Windows-only network test flake (#1461) did not reproduce locally in 30 runs and may share the cause.
Upgrading
Every behaviour a program could notice, with the change that made it:
- Library format (magic
0xffb9, #1324): every.oblmust be rebuilt with this toolchain. An0xffb8library is refused with "was built before enums were named with their bundle. Rebuild it with this toolchain." - Compiled programs: an
.obecompiled by an earlier release is refused by this runtime, which requires an exact toolchain version match; the message names the file, both toolchain versions and which side is older, and says to recompile the source with thisobc(#1387). obuin a non-ASCII install directory: a v2026.10.1oburun from an install directory whose name has a character outside the ANSI code page crashes (0xC0000409) before doing anything, and nothing in this release can reach it, so such an install must be upgraded to v2026.10.2 by hand; from v2026.10.2 onobuhandles that path (#1357).Directory->Listno longer returns.and..on Windows; it never did on POSIX (#1327).- Integer literals:
010is a compile error that suggests0o10(octal) or10;0o17is octal; a bare0bor0ois an error rather than 0;-9223372036854775808is accepted (#1355, #1345). A float literal outside the double range is a compile error (#1332). - Name resolution: an unqualified class or enum name that two
used bundles declare is a compile error; a class with noNewof its own cannot be instantiated through its parent's; a class's enum must be qualified from another bundle; implicit narrowing ofBase[]toKid[]is a compile error (#1336, #1343, #1299).programs/frameworks/openai/openai_batch.obswas the one program in the repository that relied on first-use-wins. System.Time.Datemust be written in full when bothSystem.TimeandDatabase.ODBCare in scope, since both declare aDate(#1331).Byte->CompressBr/UncompressBrareCompressDeflate/UncompressDeflate(raw RFC 1951 deflate, which is what they always produced); compiled programs keep running, source needs the new name.Web.HTTP.Server.Response'sCompression->BRis removed;DEFLATEsends the zlib format, which is what HTTP'sdeflatemeans (#1314).Byte[]->ToHexString()returns two lower-case hex digits per byte with no prefix or separators;Int[]->ToHexStringreturns[0xff,0x10](#1348).ByteBuffer->New(Byte[])copies the array;ByteBuffer->Wrap(Byte[])keeps it (#1359).Collection.Hash: iteration order (GetKeys,GetValues,GetKeyValues,ToString,Each) changes; it was never specified. Keys of different classes are always distinct even when theirComparewould say equal.SetAutoResize(false)no longer matters.XmlElement->ToStringwrites attributes in the order they were added, andKMeans->Groupkeeps its labels' order (#1341).Matrix2D->Softmaxon a matrix with several rows and several columns now normalizes each row; the old whole-matrix values were wrong (#1287).Data.JSON: a document containing a malformed number (01,1.,.5,1e) now fails to parse;JsonElement->New(Float)writes the shortest round-trip text, so request bodies carry0.5rather than0.500000(#1288, #1304).Data.JSONdecodes strings when it parses (#1403):GetValue(),GetString()andGetNames()return the text itself, so escapes such as\",\nand\u00e9are the characters they stand for. A program that calledJsonElement->Decodeon a value read from a parsed document must stop: the value is already decoded, and decoding it again corrupts text that holds a backslash (C:\newwould gain a line break). Parse then serialize is now the identity where it doubled every escape,ToString()no longer writes/as\/, and a document with an invalid escape (\x, a short\u) or an unterminated string no longer parses (GetError()names the offset).Decodeis deprecated and unchanged; it still unescapes text that holds JSON escapes.System.NLP.TF_IDF->New()smooths IDF as scikit-learn does,ln((1 + N) / (1 + df)) + 1, and scales each vector to unit length (smooth_idfandl2_normalizeare true;remove_stopwordsandstemare false). v2026.10.1 weighted by the rawln(N / df)and did not normalize, so vectors, similarities and anything thresholded on them change;TF_IDF->New(smooth_idf : false, l2_normalize : false)gives the old weighting, though the new tokenizer splits text differently. A secondFitreplaces the vocabulary instead of adding to it (#1286).Data.CSV: a row longer than the header fails the parse andGetError()names the row (#1289).Query.RegEx:a$bno longer matchesa;(?is an error unless it begins(?:;(a+)+bis slower because it now captures,(?:a+)+bis not (#1290).Query.Structured: CSV cells are typed by their whole text (1-2is a string, not 1);Averageskips non-numeric cells;a or b and cmeansa or (b and c); a query with trailing junk fails (#1291). In the new columnar engine NaN satisfies only<>, is ignored by zone maps and sorts after every number (#1351); that is a property of the engine rather than a change, since v2026.10.1's SQL path answered the same way.+ - / %are symbols in SQL, and.is a symbol between two quoted strings; both were errors before (#1367, #1366).Table->GetErrorandCatalog->GetErroranswer the calling thread's own last error (#1378).where a = bcompares two columns whenbis one (it comparedawith the text'b'), a literal may stand on the left, an Int overflow in the select list gives an empty cell rather than a wrapped number, and two Bools in an OBJECT column order false before true (#1398); anONthat does not compare one column from each side is no longer refused but tests every pair,ORDER BYmay name a grouping column the select list does not show, and an expression in a grouped query's select list,HAVINGorORDER BYis answered rather than refused (#1416, #1417). A URL cache written by v2026.10.1 is not read, so the first fetch of each URL downloads it once more (#1384).API.OpenAIResponse->GetText()returns the text of a reasoning-model reply instead of Nil (#1292).API.OllamaclearsGetLastError()at the start of each request,DeleteandCopyreport the server's error text, andCreate(name, modelfile)works only on servers before 0.5.5 (#1293).Serializer->Serializecompresses at zlib level 1 rather than 9: about 14% larger, and 5 s to 0.23 s on 32 MB;Byte->CompressZlibkeeps level 9 (#1304).- Interpolation:
"{$s}"with a NilStringor object printsNilinstead of raising (#1306). - Call stack: the default depth is 16,384 frames (
--call-depth,OBJECK_CALL_DEPTH; 256 to 1,048,576),runtime.call.depth_maxreports it, and running out of stack, native stack included, is a recoverableRuntimeErrorcompiled or interpreted. VM threads reserve 16 MB of native stack on Windows andobr/obi32 MB for the main thread (#1377). --gc-thresholdno longer grows on its own: it holds exactly as given while a major collection leaves at least a quarter of it free (so--gc-threshold=64kstill collects about every 64 KB of allocation for a program with little live data); while more than three quarters of it survive, the next major waits until the heap reaches twice the live size; and the given value returns once the live heap fits under it again.OBJECK_GC_TRACEreports the first time a program outgrows its threshold (#1311, #1383). Without the flag the threshold is twice the live heap after each major collection, never below 8 MB.- POSIX only:
SIGPIPEis ignored from VM start, so a write to a closed stdout pipe fails instead of ending the program, as in Python; Windows never hadSIGPIPE, so nothing changes there (#1373). - Sockets:
GetLastError()is Nil when the thread's latest socket call worked (#1371); a UDP client is connected to its peer and hears only it, moving to the next address its name resolves to when one is refused (#1376);UDPSocketServerbinds the dual-stack wildcard (#1373); on Windows a second TCP server on a taken port now fails rather than sharing it (#1348);TCPSecureSocket->SetVerifyPeer(strict)changes nothing and returns whether verification was required (#1349);McpServer->Serveaccepts"http_port": 0and reports the port throughGetBoundPort()(#1380). - HTTP: a 302 is framed like any other response and follows the normal keep-alive decision;
Url->Encodeescapes non-ASCII text as UTF-8 and no longer escapes~;Request->GetParamsees names that were encoded and values containing=(#1300, #1328, #1340). TableServersends tables as.otabformat 1 so clients of any version read them; a client of this version reads both (#1365). It no longer serializes requests, so a program that changes published tables from its own threads must do it betweenBeginWrite()andEndWrite()(#1389).McpServer->Serverefuses a configuration that lackshttp_port,http_session_timeout,debug,name,versionorinstructions, or holds one of the wrong type or out of range, with a message on standard error naming the key; each of those took the VM down before, the first three atServeand the strings at the firstinitialize(#1394).WebServer->Serve(filename)reports that it is not implemented.System.Time.Timermeasures wall-clock time on Linux and macOS, where it measured process CPU time (#1426).FindElements[n](Data.XML, #1444):[2]is the second matching element of each parent, as in XPath, where it used to select the first two.- Unknown columns in a single-table
selectorORDER BY(Query.Structured, #1444):select name, nope from tmakesQueryreturnNilwithGetError()set tounknown column 'nope', wherenopeused to read as empty cells; joins,WHERE,GROUP BY,HAVINGand aggregates already refused it. - API compatibility: against v2026.10.1's editor index (
objk_apis.json) no class is removed (450 classes become 498 on master and 531 with the lab work in #1426) and the only functions removed areByte->CompressBrandUncompressBr, which areCompressDeflateandUncompressDeflateabove. GetInstanceID()returns a small number assigned once per object, not its address, and never changes while the object lives (#1472). Code that printed it, or used it to order objects, sees different values;BasicCompare'sHashIDandComparefollow it, soHashiteration order over such keys changes.- TLS client reads time out after 300 seconds without data (#1471); set
OBJECK_TLS_RECV_TIMEOUT(milliseconds,0for none) to change it.
[v2026.10.1] - 2026-10-20
Objeck gains structured error handling (try/on/finally over Error objects), named arguments, method-level generics, reference parameters, expression array literals and jagged arrays; the language server is rebuilt around LSP 3.17; and more than a hundred defects found by Rosetta Code tasks are fixed.
try, on, finally and ref are contextual, so no existing name stops compiling -- and every .obl is a new format (magic 0xffb8): rebuild any library of your own with this toolchain. Standard-library parameter names are now public API, because a call can name them.Language
- Errors you can raise, catch by type, and clean up after (#1267, #1273, #1274): System.Error and its subclasses are raised with Runtime->Raise and caught with try { } on(e : ParseError) { } finally { }. Clauses are tested in order, an error no clause names goes on after finally, and return, break and continue leave through finally. The runtime's own faults are NilError, IndexError, DivideByZeroError, CastError and RuntimeError, and a failed Runtime->Assert or library call now recovers instead of ending the program. ?-> still recovers from anything, and Runtime->GetLastError() says what it was.
- Named arguments (#1263, #1268): F(data, rate: 0.1, epochs: 50) in any order, skipping any defaulted parameter, for the program's own methods and constructors and for library ones: every .obl records its parameter names, so standard-library parameter names are now public API. Arguments are still evaluated in the order written.
- Method-level generics (#1266, #1275): function : Max<T : Compare>(values : Vector<T>) ~ T, inferred from the arguments and checked against the bound, in the program and in libraries. Vector, List and CompareList gain Map<R>, so a map can change the element type: words->Map(\(s) => s->ToUpper()) is a Vector<String>.
- Reference parameters (#1277): function : Swap(ref a : Int, ref b : Int) and Swap(ref x, ref y): copy in, copy out, as Swift's inout, with 'ref' at the call site too. A local, a parameter or a field can be passed.
- Array literals hold expressions; jagged arrays (#1264, #1265): [x, x * x, Square(x + 1)] is built when evaluated, and objects of several classes make a Base[]. T[][] is an array of rows of any length, as a local, parameter, field or return, with ragged literals.
Language server
- The language server, rebuilt around LSP 3.17 (#1221-#1259): pull diagnostics, real completion documentation and MarkupContent hover, rename with validation and organize imports, hierarchical document symbols, one configuration per workspace folder, semantic token ranges and deltas, a Run code lens, links for 'use', progress reporting, and one server per VS Code window over stdio. Analysis is reused until the text changes and edits are debounced; positions are UTF-16 on every platform.
Packaging
- Debian packages for Ubuntu 24.04: sudo apt install ./objeck-ubuntu24.04-x64_2026.10.1.deb (or -arm64) installs the toolchain under /usr/lib/objeck with the commands on PATH, and apt installs the system libraries it needs; OpenCV, SDL2, unixODBC and LAME are recommended rather than required, since only the libraries that wrap them load them. The package holds the release's own Linux binaries, unchanged; apt remove takes it all out. Built from the published tarballs by
.github/workflows/release-deb.yml, which installs each in a clean container before uploading it and adding it toSHA256SUMS; design indocs/DEB_PACKAGE_DESIGN.md.
Libraries
- Libraries (#1108, #1169, #1251, #1262): Condition, Semaphore, CountDownLatch and Barrier; set algebra; Hash, Heap and sort additions; String helpers; System.Math.Rational and Complex; System.Base->ToString for every class.
Bug Fixes
- JIT and recovery fixes (#1190, #1198, #1205, #1260, #1271, #1272, #1273): a deferred load materialized in one branch arm; an error in a tail-call loop and a bounds error mid-argument-list crashed recovery; more than 16 nested guarded calls lost their inner handlers; a NaN compared as equal (x64) and as less (ARM64); negative array sizes; ?-> deferred correctly from a bridge interpreter.
- Compiler fixes (#1168, #1197, #1204, #1207, #1208, #1249, #1261): wrong code in call chains and closures, generic type arguments through chained calls, primitive receivers, unary minus on a parenthesised operand, critical() locks released on return, a tail-call label that collided with an inlined callee's (an infinite loop at -opt s3), a returned literal swapped with a leaving block's, and typos that used to declare a variable silently.
- VM and library fixes (#1192, #1193, #1195, #1199, #1200, #1201, #1211, #1212, #1240, #1257, #1268): Float->ToString of large values, GMT dates and pre-1970 times, file I/O edge cases (Flush, ReadLine, empty files), multi-dimensional serialization, reflection metadata, String edge cases, "\e" as ESC, Byte arithmetic, and Ints no longer printed with a locale's digit grouping on macOS and Linux.
Platforms
- Linux ships an environment helper and an optional desktop entry (#1088): the Windows and macOS launchers set up an environment and open a shell, and the Linux tarball had no equivalent -- the README asked you to export
PATHandOBJECK_LIB_PATHyourself. Sourcingenv.shdoes that,objeck-shellstarts an interactive shell with it applied, andinstall-desktop.shadds an "Objeck" entry to the application menu. It writes only underXDG_DATA_HOME, so it needs no root and cannot collide with a distribution package, and it is a separate opt-in script rather than part of extraction. The entry setsTerminal=true, so the desktop environment supplies the terminal instead of this guessing which emulator is installed.Execis quoted, because the Desktop Entry specification splits that value on spaces and an install path with a space in it would otherwise launch nothing.
Build
- The MSYS2/MinGW build had not compiled for some time, in five independent ways (#1087, #1089, #1091): four of the five were the same mistake --
_WIN32used where_MSC_VERwas meant. MinGW-w64 defines_WIN32and compiles with GCC, so every MSVC-only construct behind that guard reached a compiler that has never had it.core/vm/arch/memory.h's write barrier usedReadNoFence64and_InterlockedOr64, which took outobr,obd,obiand the module at once, since every translation unit includes that header.core/lib/diags/diags.cppused__try/__except, solibobjk_diags.dll-- the LSP's native half -- could not be built at all. The VM linked nobcryptalthoughcommon.cpprelies on a#pragma comment(lib, ...)that only MSVC obeys, so every link containing it failed onBCryptGenRandom. A stalesys.aproducedundefined reference to 'logger'on a tree that compiles cleanly, because the copy that publishes the archive to its two consumers lived inside the recipe that builds it and therefore ran only on a rebuild. The deploy scripts also had no failure handling whatsoever: every stage built and then copied unconditionally, so one bad header produced two hundred lines of consequences ending incp: cannot stat. They now use the same progress UI as the POSIX deploy, check prerequisites up front naming thepacmanline that installs what is missing, and reach 18 of 18 stages. - The ONNX and OpenCV bindings compile against OpenCV 5 (#1091):
contourArea,boundingRect,getRotationMatrix2D,approxPolyDP,estimateAffinePartial2DandLMEDSmoved intoopencv2/geometry/2d.hppandopencv2/geometry/3d.hpp, neither of whichopencv.hppincludes. Nothing left namespacecv, so this is two version-guarded include lines rather than the Homebrewopencv@4pin an earlier release reached for. - The JIT archive compiled one source twice, with different flag sets (#1092): each backend directory named
../jit_common.oas a prerequisite of its archive and carried a%.o: %.cpprule matching it, while the object is actually built by the parent makefile inside thejit_common.asub-make. Somakehad two ways to produce one file and used both. Run serially that is waste and the second copy wins, which leaves the archive carrying an object built with the wrong flags; under-jthe two compiles race andarcan reach the file mid-rewrite, which is thear: ../jit_common.o: No such file or directoryanother machine hit on this tree. The object is now named only inside the recipe, wheremakehas no way to try to build it, whilejit_common.astays a prerequisite so the sub-make that does build it still runs first; thearcommand line is unchanged. Found withmake -n, which shows it deterministically -- six clean parallel rebuilds passed both before and after the fix.
Release
- A manifest regenerated after Windows signing is signed again (#1081): signing rewrites the installers, so
SHA256SUMSis rebuilt over them, which invalidates the signature -- andoburefuses a manifest whose signature does not verify, so this is the difference between a verifiable release and one that reports tampering. The workflow meant to repair it had never once completed: it signed the manifest, verified it against the committed public key, and then could not upload it, becausecd "$WORK"moved out of the checkout and everyghcall after that lost the repository it was inferring. v2026.10.0's manifest was re-signed by hand. The step now carriesGH_REPOexplicitly. - Every Sourceforge link in the repository returned "Page not found" (#1085): the project's unix name is
objeck-lang, notobjeck, so the README's "Alternative" download link, the one indocs/release_process.md, the URL the publish job prints on success and the one it writes into the run summary all 404'd. The real listing was complete throughout -- all 12 assets of v2026.10.0 plus both source archives -- mirrored by a GitHub webhook rather than by the upload job, whose SFTP path carried the same wrong slug and would have created nothing. Also ignoressign_release.cmd's staging directory, which is removed on the way out but not when the script exits early: v2026.10.0 left 78 MB of signed installers untracked in the repository root, exactly wheregit add -Awould have taken them. - The objeck.org deploy covered half the site, and could not run (#1084): v2026.10.0 published with
objeck.org/api/latestcorrectly at v2026.10.0 while the page a reader actually lands on still offered "Download v2026.9.7". Two unrelated causes, neither of which would have gone red.deploy_website.shonly ever deployed the generated API reference;docs/web/holds the site's own pages and nothing deployed them, so every release depended on someone remembering an undocumented hand-upload. It now does both halves by default, with--api-onlyand--site-onlyto narrow it. The site pages cannot use the reference's upload-beside-and-swap, because they live directly in~/public_htmlbesideapi/,.htaccessandobjeck-release.pub, so they go per file: hashed against the live copies so only what differs travels, staged remotely, hash-checked there, and moved into place one atomic rename at a time, leaving a remote file with no local counterpart alone rather than treating it as stale. It also refuses areadme.htmlwhose Download button names a version other than the one being deployed -- that button is hardcoded, it sat on v2026.8.1 through the whole of v2026.8.2 and on v2026.9.7 while v2026.10.0 was live, and uploading a page that offers the previous release is worse than leaving the old one because it looks freshly deployed. Separately,release-publish.yml'sdeploy-docsjob had no checkout at all. - The running release draft was titled with a variable rather than a tag (#1082), and the four download markers were advanced to v2026.10.0 once that tag existed (#1083).
Infrastructure
- Release verification runs on all five platforms from one dispatch (#1095): the gate wanted five machines booted by hand, although all five targets have had native hosted runners for a while and
ci-build.ymlalready exercises them. What was missing was the record:rc-results/is gitignored, so each verdict file existed only on the machine that produced it, and v2026.10.1 reached a point of having windows-x64 passing at one commit and linux-x64 at another, with three platforms unverified.verify-platforms.ymlruns the sameverify_platform.sh/.cmda maintainer runs locally, on each selected target, and its summary job refuses to pass unless every requested leg reportsverdict=PASSat the same commit -- the check both scripts' headers attribute to atools/cicd/pre_release.shthat does not exist in this repository. Rather than copyci-build.yml's dependency setup, it revives.github/actions/setup-objeck-deps, a composite action nothing used and that had drifted badly enough to be a trap: it was missingexpect, without which the debugger suite exits 0 having run nothing,libsqliteodbc, the GL packages, the ODBC test DSN, the Xcode selection and the entire Windows vcpkg install. - A checkout made on Windows cannot build the POSIX ONNX library (#1094): nine paths in this repository are stored as git symlinks -- the ONNX Runtime SONAME links for both Linux architectures and the macOS LAME and SDL dylibs -- and a clone made on Windows has
core.symlinks=false, so git materializes each as a small text file holding its target's name.libonnxruntime.sois 19 bytes of ASCII readinglibonnxruntime.so.1, andldreported "file format not recognized; treating as linker script" 169 seconds into a build, two minutes of which had been spent compilingonnx.cpp. There is nothing to fall back to, since the SONAME link is needed to load the library as well as to link it, so the gate gained asymlinksprecondition that catches all nine in milliseconds, andrequire_vendored_ort-- which existed for this class of failure but tested for a directory -- now checks the library itself. - The gate summary read CRLF verdicts as failures (#1096): the first five-leg run had every leg pass at one commit and the summary failed it, reporting
::error::windows-x64 verdict=PASS.verify_platform.cmdwrites its result file with cmdecho, so the Windows legs' files are CRLF while the POSIX legs' are LF, and[ "$v" = PASS ]was comparing"PASS\r". The same CR made the two Windows commits differ from the three POSIX ones as strings while naming the same commit. One bug, counted three times. - The GC stress probe's diagnostics had never fired (#1086), and the
bump-versionskill no longer tells you to advance the README download badge during a bump, which is the bug #786 was opened to undo (#1093).
[v2026.10.0] - 2026-10-11
Any ONNX model can be run from Objeck, either in-process or on a model server, behind one interface; releases now ship a signed manifest; System.ML gains what it needs to evaluate a classifier under a false-positive budget; and a compiler bug produced an infinite loop at the default optimization level.
API.Onnx or API.Inference must add -lib models and use API.Models. The two libraries each defined their own Tensor; there is now one, in a library they share. Library dependencies are not transitive in Objeck, so this is a compile error until the -lib list is updated -- including for programs that never mention a tensor. Two helper methods containing the same construct were inlined into one shared label space, so the second one's jump resolved to the first one's label -- a backward jump into already-executed code. -opt s3 is the default, so this needed no flag to reach. Separately, out-of-fold scores, metrics at a chosen threshold, bootstrap confidence intervals and per-group recall make the whole evaluate-under-a-budget protocol expressible without hand-written glue.Compiler
- A program could compile to an infinite loop at the default optimization level (#1037): the s3 inliner advanced its label offset only when the same method was inlined a second time. Labels are numbered from each method's own start, so two different callees containing the same construct carry the same numbers -- two helpers each evaluating
->Abs()is enough. Under one shared offset the second body'sLBLlanded on the first body's and itsJMPresolved to the first, which is a backward jump into already-executed code. Correct at-opt s0,s1ands2, and unaffected by disabling the JIT, so the bad jump was in the bytecode. The offset now advances for every inlined body, and the conflict check -- which compared a candidate only against the caller's labels, and so could not see an inlined-against-inlined clash -- now records inlined labels too. Rebuilding all 32 libraries with the fixed compiler changed exactly one,misc.obl, whose committed bytecode was therefore wrong; the suite passed against it, so no exercised path looped. - A generic array parameter did not resolve across a library boundary (#998):
Vector[]<T>as a parameter compiled inside a library and callers in that same library reached it, but a program linking the library was told the method did not exist.TypeParser::ParseGenericsended its loop on the closing>rather than past it, so the parameter path read that>where it scanned for the dimension and left the dimension at 0.ParseType, which decodes return types, already stepped past it -- which is why the same type worked as a return value and failed as a parameter. A reader-only fix: the.oblencoding was always correct.
Runtime
- A recovered
?->compared wrongly againstNilwhen the comparison was written inline (#1067):if(x?->y() = Nil)took the wrong branch once the failing method was hot, whiler := x?->y(); if(r = Nil)was correct in every mode -- so the idiom?->exists for was the one that broke, and only after warm-up, which means a program could behave correctly in testing and wrongly in production. Recovery itself was never at fault: the handler storedNiland a local read it back correctly, which is what made the two spellings disagree so confusingly.TryErrorRecoverywas ASSIGNING the handler's saved operand-stack position rather than treating it as an unwind target, so a position above it was raised instead of lowered and the comparison read a stale slot. The saved position is now a floor, never a value to assign.programs/regression/try_chain_inline_compare.obsholds both spellings to the same answer, deliberately without a JIT opt-out: the two modes agreeing is the entire property under test, so a marker there would remove the only thing it checks Try()now recovers from an error raised inside compiled code, so the language's error semantics no longer depend on what happened to be JIT-compiled (#925): #900 madeTry()recover from a failing cast, but only where the interpreter owned the error. Inside a compiled method the cast was raised in a JIT callback, which is static, holds no interpreter, and so could not see the handler stack --jit_common.cppbuilt a THROWAWAYStackInterpreterjust to print a stack listing, which was the clearest evidence of it. Whether a guarded chain recovered or aborted therefore depended on how many times the failing method had been called: the same program, run twice, could do either. Demonstrated against the pre-fix build, wheretry_recovers_cast.obsaborts with exit 1 compiled and passes with exit 0 interpreted. Three changes close it. The handler stack moved fromStackInterpreterinstance fields to per-thread storage -- aTry()is inherently per-thread, and the hazard was that all four constructors zeroed it whilejit_common.cppbuilds interpreters at three sites on a thread already running Objeck code, so the naive move would have wiped a live handler the moment compiled code took any callback.StackCallbackBodyreturns a status instead of callingVmExit, and both backends test it and leave through the epilogue, where the interpreter already recovers a guard stub's status. The status isint64_t, notlong: on Win64longis 32 bits, so -5 reached the return register zero-extended as0x00000000FFFFFFFB, which the 64-bit compare reads as positive -- the branch never fired and the method carried on past a reported error with nothing pushed. The diagnostic is printed only when no handler is active, so a recovered cast is as silent as in interpreted code and stdout is identical between the two. Compiled recursion is still fatal and is not reachable this way: compiled frames consume the real C stack, so the process dies in the OS with no frame left to return a status from;try_recovers_depth.obskeeps its opt-out and says why. A JIT-to-JIT call whose callee fails now recovers too, which was the third and last gap:EmitNativeCallSitecalledJitNativeCallError, which printed and exited, so a compiled method calling a compiled method that failed could not recover even though the failing frame had already returned and the handler was intact. The call site now branches to its own teardown with the status still in the return register -- the same exit a bridge callback's status already took, so the two directions share one handler -- andCallCompiledreports its callee's status through an out-parameter instead of exiting. A status noTry()takes reaches the interpreter's own switch, which reports per status and unwinds, so the unguarded case gains a stack listing it never had andJitNativeCallErroris deleted rather than left dead. Frame bookkeeping had to be fixed first and is what made the first attempt fail: the native prologue pushes a frame record, the only matching decrement was in theRTRNhandler, and a guard stub never reachesRTRN-- so a natively entered method that failed left its record registered and the unwinder walked entries that were not frames. Nothing could see that until now, because recovery pops frames to the handler's depth and swept it away, and the unguarded path exited without unwinding; the pop therefore moves to the head of the teardown, the one point every exit reaches. On ARM64 that placement is forced rather than chosen -- its epilogue computes branch distances as literal constants, so a block inserted above the teardown shifts all ten of them--nurserynow reduces memory on Windows instead of being ignored (#841): the young region was reserved and committed at its full 128 MB, so lowering the limit changed where the collector drew its boundary but not what the process charged to the system -- POSIX got this for free, becausemmap(MAP_ANONYMOUS)commits lazily. Windows now reserves the full region and commits only the configured limit. The growth half is still open: the AMD64 JIT inlines the nursery bump, so raising the limit at runtime needs a commit check emitted into that fast path on every backend that inlines it.- A native library could store an object the collector tracks without a write barrier (#864): a native library may store an object it allocated anywhere, because
AllocateObjectNativeputs those in the old generation. An object it received is a different matter: storing that reference without a barrier leaves the collector unaware of it, and three sites in the ONNX binding did exactly that. The VM now exposes a barrier for native code to call,docs/native_interface.mddocuments when it is required, andtools/cicd/check_native_write_barrier.pyholds every native store to it as part of CI -- so the next missing barrier is a failed build rather than an intermittent collector crash.
ONNX
linux-arm64shipslibobjk_onnxfor the first time (#1048):API.Onnxwas unavailable on that platform and two regression tests skipped for it, because only an x86-64 ONNX Runtime was vendored --deploy_posix.shnoted that the copies "put a 16 MB x86-64libonnxruntimeinto the aarch64 tree", which is what the v2026.9.1 tarball carries. That was never a platform limitation: ONNX Runtime publishes an aarch64 Linux build, and one for the version already pinned, so no version bump was needed.build.shanddeploy_posix.shalready looked for it andverify_native_libs.sh's own comment already said to vendor it; the directory was simply empty. The arm64 exemption is removed in the same change, becauseverify_native_libs.shdeclared onnx optional there -- so a leg that failed to build it would still have exited 0, and a green build would have been no evidence the runtime links.- Any ONNX model can be run, not only the eight families with a wrapper (#1047):
API.Onnxhad eleven native entry points and eight of them were per-model-family inference functions over 3,100 lines of per-family C++.API.Onnx.Sessionis the chokepoint they never had -- hand it named tensors, get the model's outputs back as named tensors -- withGetModelInfo()reporting the names, element types and shapes a model expects, and a dimension the model leaves symbolic reported as -1 rather than guessed at. The caller never states an element type: tensors cross asFloat[]and the model's own declaration decides what each input becomes, so FP32, FP16, INT64, UINT8 and BOOL models are driven identically; anInt[]tensor is exact to 2^53 and refused rather than rounded past it. Preprocessing and decoding move above the native layer, into Objeck, where they can be written once and reused -- eight verticals with no chokepoint is why #1028 and #1030 each had to be fixed eight times. Verified against real models rather than argued for: the generic path andResNetSessionagree exactly onresnet34(confidence gap 0.000000), and a Phi-3 prefill runs through it with 67 inputs -- three INT64 and sixty-four empty FP16 cache tensors -- turning "The capital of France is" into "Paris". The eight per-family classes all stay. - A failed session was undetectable, and two families then dereferenced Nil (#1028):
onnx_new_sessionset its return slot only on the success path, so a failed session returned whatever the caller had put there. It came out as 0 becauseIntRef->New()defaults to 0 -- a real failure signal resting on the caller's initialisation, which is why it went unnoticed. Nothing exposed that signal, so a dead session was indistinguishable from a live one; inference then returned without writing its result slot and YOLO and ResNet calledSet()on the Nil. All seven session classes now answerIsOpen()--Phi3VisionSessionrequires all three of its sessions, andFaceSessionhas a separateIsRecognizerOpen()because the recognizer is optional by design -- and both dereferences are guarded. A new regression test points a session at a path that does not exist, which is the one ONNX failure path testable without shipping a model. - The FP16 input tensor wrapped a buffer that had already been freed (#1045):
make_tensor_match_input_typeconverted into astd::vector<uint16_t>local to itself and returned anOrt::Valuewrapping it. BothCreateTensoroverloads it uses take, in the ORT header's own words, "a user supplied buffer" -- ORT wraps the pointer and does not copy -- so the vector was destroyed on return andRunread whatever had replaced it. The FP32 branch of the same function is correct, which is why this survived: it wraps the caller's own buffer, and only a model whose declared input is FP16 reaches the other branch. No model in this tree is FP16. The buffer is now a required non-const reference parameter, so a caller cannot forget it and cannot pass a temporary either; the signature enforces the lifetime. - Closing a session twice corrupted the heap, and
IsOpen()lied after the first close (#1046):close_sessiondeletes theOrt::Sessionand nulls its own local copy, which the caller never sees, and none of the seven per-familyClosemethods cleared the handle. SoIsOpen()-- which #1028 had just made the documented way to tell a live session from a dead one -- stayed true forever, and a secondClosewas a double free: reproduced againstyolov11-ft.onnxas0xC0000374on one run and0xC0000005on another, with nothing printed after it. Ten handles across seven classes now clear, andClosereturns early on a zero handle. - Two session-creation failures still went unreported (#1044): #1028 fixed the
catchand missed two early refusals -- an execution provider the build cannot supply, and a CUDA build whose linked onnxruntime does not carry CUDA. Both printed a diagnostic and returned without touching the result slot, reading as failure only becauseIntRef->New()defaults to 0, which is the exact fragility #1028's own message complained about. All four exits now write it. - Every inference printed to the program's stdout (#1030):
common.hwrote progress and timing tostd::wcoutat thirteen sites spanning every model family -- four behind a// #ifdef _DEBUGthat had been commented out and never restored, eight never guarded -- plus the CoreML fallback notice inonnx.cpp. All now write tostd::wcerr, where every error path in the same file already did. It matters beyond tidiness:run_differential.pycompares stdout across optimization levels and JIT settings, so a timing value that changes run to run makes any ONNX test diverge by construction. The sixteen inert commented-out guard directives are removed with it.
Libraries
- One
Tensorand oneEngineinterface, shared by both backends (#1062):API.OnnxandAPI.Inferenceeach defined their ownTensor, which was deliberate and temporary -- the generic ONNX run shipped before a shared library existed.API.Models(models.obl) now holdsTensor,TensorSpec,EndPointand anEngineinterface thatAPI.Onnx.OnnxEngineandAPI.Inference.HttpEngineboth implement, so calling code names a backend once rather than at every call site. This is a breaking change: every program linkingonnxorinferenceneeds-lib models, because library dependencies are not transitive -- including programs that never touch a tensor, which the regression suite established rather than the design. Merging the two tensors also removed a limit that should never have applied:API.Onnx.TensorconvertedInt[]toFloat[]at construction and refused anything past 2^53 there, imposing the native crossing's constraint on an HTTP server that receives integers as JSON integers with no range limit at all. Integers stay integers now, andAsFloats()refuses at the one boundary that cares. API.Inference: a model server over HTTP (#1051): the sibling ofAPI.Onnx-- the same models, reached over HTTP rather than loaded in-process. For a model that is not an ONNX file you have locally: served from a GPU machine, a TensorFlow model you would rather not convert, or anything already deployed behind Triton, KServe, TorchServe or TF Serving. Two wire protocols, because no single one covers the field: KServe v2 (OPEN_INFERENCE, flat data with shape and datatype stated) and TF Serving v1 (TF_SERVING, where the nesting carries the shape and there is no datatype field).Client->DescribeRequestreturns the body the client would send without sending it, which is usually faster than reading a rejection -- the two protocols shape the same tensors very differently and a tensor nested to the wrong depth looks identical in Objeck. A failed call returns Nil with the reason inEndPoint->GetLastError(), and a response body that will not parse is reported verbatim, since both protocols report errors as JSON and anything else means a proxy or a truncated response answered. 49 assertions, none of which need a server.API.Ollamaadvertised streaming completions and had none (#1027): the bundle comment claimed support while all eight requests the library can make send"stream": false, with no alternative path.code_docgenerates the published API pages from that comment, so the claim shipped to users. It now states the actual behaviour -- a request completes before returning, so a long generation produces nothing until it finishes -- which is more useful than silence, since first-token latency is most of the perceived speed of a local model.
Machine Learning
- Out-of-fold scores, a fold row mapping, and strata keys (#1035):
CrossValResult->GetOofScoresreturns one score per row from the fold model that did not train on it, which is what a threshold has to be chosen from if it is not to leak a row into its own score.FoldSplit->GetTestRowsreports which original rows a fold's test matrix holds:Splitfills it by walking rows in ascending order whileStratifiedKFoldreturns a shuffled index vector, so pairing a score with the fold vector's k-th index silently scrambles them -- measured on a 3000-row set, a pooled AUC of 0.997 read 0.539 while every per-fold AUC still read 0.99.Evaluategains an overload taking a stratification key, so folds can preserve the distribution of attack families or any other sub-population rather than only the binary label.LogisticRegressionimplementsScoreModel, so a linear baseline goes through the same path as the tree classifiers. - Metrics at a given threshold, and bootstrap intervals (#1036):
Metrics->AtThresholdreturns aThresholdResult-- the four confusion counts with recall, false-positive rate, precision and accuracy read off them -- for one supplied cutoff, the inverse ofRecallAtFpr.Metrics->Bootstrapresamples scored instances in pairs through aScoreMetricinterface (AucMetric,RecallAtFprMetric,AveragePrecisionMetric) and returns aConfInterval. A resample can draw a single class, which leaves AUC undefined; those are discarded rather than averaged in, and the usable count travels with the interval, because an interval built from a fraction of the requested draws is not the interval that was asked for.Metrics->Percentileis exposed for statistics the bundle does not implement. - Base-rate precision, label shuffling, and per-group recall (#1038):
Metrics->PrecisionAtBaseRatereprojects a measured operating point onto an assumed prevalence, with an overload taking aThresholdResult; documented as a bound rather than a prediction, since it assumes both rates carry over.Metrics->ShuffleLabelsis a seeded permutation for the shuffled-label control, returning a copy so the real labels survive to score against.Metrics->RecallByGroupreturns aGroupRecallper group carrying the positive count beside the rate, and reports a group with no positives rather than omitting it.Metrics->UnseenGroupsis the set difference for categories never trained on.
Infrastructure
- The release key is published where the repository cannot vouch for it, and a weekly job checks it (#723, phase 4):
oburefuses a manifest it cannot verify, and its refusal message sends the reader to the public key -- which, until now, they could only obtain from the repository they were trying to verify. That proves nothing. The key is now served from https://www.objeck.org/objeck-release.pub, a different host from both this repository and the release assets, byte-identical to the committed copy (113 bytes, sha256864c6a99...6c04). Two things already claimed this was true while it was not --README.mdand the message compiled intoobu-- so a user who had just hit a verification failure was sent to a page without the key. Both now name the exact URL rather than the site, andobusays which copy to prefer. The published copy is also the one nothing generates: it is uploaded by hand, lives outside the repository, and so no commit-time check can see it drift -- the only thing that would have noticed a stale one is a user comparing digests, which is exactly the person a stale key misleads.verify-signing-credentials.ymlgained a weeklypublished-keyjob for it, which fails on a mismatch, only warns when the site is unreachable (a site briefly down is not a credential problem, and a guard that cries wolf gets switched off), and usescurl --failso a 404 body is not hashed as though it were a key -- an error page has a perfectly good digest, it is just the wrong one obu's install lock followed a symbolic link (#1075):.obu.locksits in the install root, so anything able to write there could plant a symlink at that name andobuwould follow it --openwithO_CREATand noO_NOFOLLOW, creating the target if it did not exist and holding it open for writing. Nothing is ever written through that descriptor, so the reach is creation of a path of the attacker's choosing at mode 0600 owned by whoever ranobu, plus an open handle: an unexpected-file-creation and denial-of-service primitive rather than disclosure or execution, and it needs an install root writable by someone other than the user runningobu-- a shared install, or a loose umask, withobuthen run by the victim. Measured rather than argued: withoutO_NOFOLLOWthe updater suite leaves the symlink's target behind. The lock now refuses a symlink, compares the inode against the name itself withlstat, and reportsELOOPdistinctly instead of announcing a tampered install root as "another obu operation is in progress". Coverity's time-of-check/time-of-use finding on the same function (CID 1680315) is a false positive and was dismissed as intentional: thestatit flags runs after the descriptor is open and exclusively locked and decides only whether to keep or discard it, which is the standard idiom for makingopen+flocksafe when release unlinks. It is also load-bearing -- with a symlink planted, that re-check is what makes the case fail safe on an unhardened build- A substituted release manifest can no longer pass unnoticed (#723):
SHA256SUMSis served from the same place as the assets it describes, so anyone able to replace an asset could replace the manifest to match --obuthen verified the substituted archive against the substituted manifest and reported success. The Linux and macOS archives carry no platform signature at all, so for those the manifest was the only integrity claim, and it was unsigned. Releases now shipSHA256SUMS.minisig, an Ed25519 signature made in CI, and the signing step verifies its own output against the public key committed to the repository before uploading it -- a secret that did not match would otherwise be discovered by a user runningobu update.obuthen requires it:updateandverifyboth check the signature against the key compiled into the binary before reading a line of the manifest, and a missing, unparseable, truncated or wrong signature refuses the operation rather than falling back to hashes alone. The Ed25519 verification is TweetNaCl, vendored unmodified fromtweetnacl.cr.yp.towith its provenance recorded, because a verifier with a bug that returns true is a silent total failure -- the feature looks present and protects nothing -- and that is not a risk worth taking for code nobody here would be better at writing. It is held to behaviour in both directions bytools/cicd/test_obu_verify.py, against a fixture signed with the real release key: a genuine signature accepted, and a missing, tampered, truncated, unparseable or prehashed one rejected, plus a tampered trusted comment and a substituted manifest. Regressing the verifier to always return true fails nine of its ten cases, and the updater suite's own two signature cases besides. Phases 2 and 3 ofdocs/release_integrity.md, in that order deliberately -- anobudemanding a signature before any release carried one would have rejected every release it could see. A release published withoutSHA256SUMS.minisigis now one noobuwill install, so the publish fails when the signing secret is absent unless signing is explicitly declared manual. That also makes the manifest's post-publication repair a two-part operation: Windows MSI signing happens after publication, on the machine with the hardware token, and rewrites the installer bytes, soSHA256SUMSis regenerated over the published assets -- which invalidates the signature made during publication.resign-manifest.ymlsigns the published manifest again, in CI, so the secret never reaches the signing machine;update_sha256sums.ps1andpost_release.shboth request it and fail if it does not succeed,post_release.shgate[4b]verifies the published signature against the published manifest usingobuitself, andtools/cicd/check_manifest_resign.pyfails the build if a third tool ever replaces the manifest without arranging for a signature over it. Both existing tools did exactly that, which before phase 3 was merely useless and after it would have made every signed release uninstallable. - The LSP offered a quick fix naming the wrong library (#1052): the class-to-bundle index is keyed by short class name and the generator emitted one JSON key per class definition -- duplicate keys, of which a parser keeps the last. Twenty-two names are defined in more than one bundle (
EndPointin four), so 433 definitions arrived as 406 and the rest were simply gone. This is an applied edit, not a hint: forClientthe surviving entry wasData.JSON.RPC, so anyone who meantAPI.Inference.Clientgotuse Data.JSON.RPC;andjson_rpc.oblwritten into their build file, leaving the program still not compiling. Each name now carries every bundle that defines it and the editor offers one fix per candidate. - The quick-fix path had no coverage at all (#1055): its only test asserted that the handler ran, and returned an empty array -- for
Vector, its own fixture's class. The fixture declaredv := Vector->New(), and the native matcher needs a resolved type, which an inferred local does not have, so the test could not reach the path for any class. That is how #1052 shipped. Two fixtures now assert what the fix produces, including one for a name defined in two bundles, which fails against the old index. - The editor API index had silently fallen four releases behind (#1050):
objk_apis.jsonis what the language server answers completions and hover from, and the copy underclients/vscodeis packaged into the.vsix. Both are tracked, both are generated, and nothing regenerated or verified either -- so the index changed only when someone remembered, and nobody had. Regenerating tookAPI.Onnxfrom 48 entries to 58: the whole generic run API shipped with no editor support, and severalSystem.MLreleases before it had too.check_doc_lists.pydoes not cover this and is not meant to -- a library can be in all six lists and still be absent from the index, which is the state the tree was in. CI now regenerates and compares the class set rather than the bytes, since the index is emitted on whatever machine the maintainer used and a guard that reddens CI over whitespace gets disabled. Its own self-test carries ten cases, each confirmed to fail against a deliberately regressed checker. - The generated POSIX Makefiles are build output, not source (#1005):
deploy_posix.shcopies an architecture's Makefile over four working-tree paths, two of which were tracked, so building for one architecture left tracked files modified while the other two were not ignored and could be committed by a bulk add. - A missing
expectskipped the debugger suite and reported success (#1022): the runner exited 0 whenexpectwas absent, so every debugger test was skipped and the leg passed. It now fails unlessOBJECK_ALLOW_SKIP=1is set, andexpectis listed as a test dependency. - A refused Coverity upload was read as a submission (#1031): Coverity declines an upload with HTTP 200 and a plain-text reason, so
curlexits 0 and--fail-with-bodynever tripped.cov_scan.shprinted "Submitted" over a rejected upload and then deleted the intermediate archive its own retry hint had just promised was kept. The response body is now required to say the submission succeeded. - The release body gate flagged real shell as an unsubstituted variable (#1020): the check scanned the whole body including fenced code, so a legitimate
$PATHin an example read as a template variable that had not been filled in.
[v2026.9.7] - 2026-09-28
Supervised classification becomes usable end to end, three defects that all treated a Bool array as int-width are fixed, and float comparisons involving NaN follow IEEE. System.ML could fit a model but not honestly evaluate one: there was no way to hold a scaler's statistics across a train/test split, no stratified fold, no metric that took a score rather than a decision, and no classifier that split continuous features. All of that is here, with a cross-validation runner over a common interface. Separately, a Bool[] returned from a method was mishandled in three different places — one refused to compile, one silently read the wrong bytes, and one aliased instead of copying. And on Windows every comparison involving NaN returned true, so NaN compared equal to 1.0 while being both less than and greater than it.
Machine Learning
- A scaler can now learn statistics once and reapply them (#980): the static
StandardScalercould only standardise whatever matrix it was handed, which cannot express the one correct thing to do across a train/test split. Scaling test data by its own statistics leaks the test distribution into the evaluation and quietly flatters any model measured at a fixed false-alarm rate.FeatureScalergainsFit,TransformandFitTransform TableEncoderturns a mixedCsvTableinto a model-ready matrix (#981): the value vocabulary is fixed atFit, so a test split missing a category, or carrying one the training split never saw, encodes against the same columns instead of silently changing the feature spaceStratifiedKFoldkeeps the class ratio, from a seed (#982):KFoldshuffles with the global generator and ignores labels. On imbalanced data an unseeded split cannot be reproduced, and an unstratified one lets the rare class land unevenly, so a fold may hold too few positives to measure anything at a low false-positive rate- Score-based metrics (#983):
RecallAtFpr,ThresholdAtFpr,AucRoc,AveragePrecision,RocCurveandPrCurve. The existing metrics takeBoolpredictions, which are already thresholded, and aBoolarray cannot say what recall would be at a different false-alarm rate. Instances sharing a score share an operating point, because splitting them would invent an ordering the scores do not express - Classifiers that split continuous features (#984, #986, #987):
DecisionTreeClassifier,RandomForestClassifierandGradientBoostedClassifier. The existing trees takeBool[,], so continuous data had to be quantile-binned first — and the binning discards the thresholds the tree exists to find. All three return a score rather than a label, so they can be evaluated at a chosen false-alarm rate - A cross-validation runner (#999):
ScoreModelis a common interface the three classifiers implement, andCrossValidation->Evaluateruns the folds, fitting the scaler on the training split only.CrossValResultreports per-fold values and the mean and spread, because a single fold's AUC is extremely noisy when the positive class is rare - Seven unguarded ratios in
System.ML(#989): two were reachable from public API with ordinary data and took the VM down —LinearSolver->Calculateon a target column that happened to be constant, andScoreon a zero-row matrix in four classes, which guarded their arguments againstNilbut not against a matrix that was merely empty
Compiler
Three defects in one family: a Bool array returned from a method was treated as int-width in three separate code paths. A Bool[] held in a local was correct throughout, which is what made each of them look like something else.
if(obj->Predict(x) = Nil)would not compile when the call returnedBool[](#985): rejected as "Invalid mathematical operation", while the identical comparison onFloat[], or on aBool[]local, compiled.IsBooleanExpressionapplied its array-dimension test only to variables- An indexed
Bool[]call result read several bytes instead of one (#988): aBool[]is allocated asNEW_BYTE_ARY, but the path for subscripting a call's result classified it as reference width. The value then depended on neighbouring elements and on the platform's memory layout, so a different expression misevaluated on each backend — x64 got the left operand of&wrong, macOS ARM64 got|, Windows ARM64 got=against a literal. A silent wrong answer, not a compile error Bool->New[src]aliased instead of copying (#995):BOOLEAN_TYPEhad no arm in the array copy constructor's switch, so nothing was emitted and the source reference was left on the stack as the "copy". Writing through the source then changed it- A library's array copy constructor crashed the compiler (#958): the call was emitted with class ids local to the library being written, and a consuming build then dereferenced a map it had never heard of — a bare SIGSEGV out of
obcwith no message and no output file - A toolchain-version mismatch named the wrong side (#1010): the error read "the LIBRARY appears to be compiled with a different version", which sends you to rebuild libraries that are already current — the stale side is usually the reader. It now says which side is which and names both versions, after costing two separate sessions a rebuild each
obcnames the library that would supply an unresolved class (#1015):Unable to resolve external library class: X; check library pathwas true and unactionable -- the path is almost always right and one entry is missing from-lib, and finding which one meant greppinglib_src. The linker now scans the libraries it did not load and saysAdd it with '-lib net_server'- The
@weblibrary group never worked (#1014):-lib @webfailed on any program, including one that never touched HTTP, because the group omittednet_server.obland the linker loads exactly what a group lists. The other six groups were fine - A
@group expanded or not depending on where it sat in the list (#1016): building a library,-lib @stdworked and-lib @std,misctried to open a file named@std.obl. One of the two expansion paths carried a!is_libguard the other never had; both arrived in the same commit, so the difference was an oversight. Groups now expand in any position, for either--target, and are documented indocs/cli_options.md--obc's own usage had never mentioned@at all
Runtime
- Every comparison involving NaN returned true on Windows (#1000): NaN compared equal to 1.0 and was simultaneously less than and greater than it. Two separate causes in opposite directions. The interpreter's C++ is IEEE-correct, but the VM is built with
/fp:fast, which permits assuming operands are not NaN — and since that flag is set only in the Windows project, the same program answered differently on Linux and macOS. The amd64 JIT had the opposite error for equality alone, becauseucomisdsets the zero flag for both equal and unordered and only the parity flag separates them - The old generation moved to open addressing (#871): Windows
binarytreeswent from 2.392s to 1.789s - Twelve library divisions aborted the VM on ordinary input:
CsvColumn->Average(1, 1)— the average of an empty range on a populated table — was confirmed as a live crash before anything was changed
Updater
obu verify <archive> <SHA256SUMS>(#723): the integrity checkupdatealready performs, exposed for anyone who downloads withcurlinstead. A mismatch and a check that could not be carried out are different exit codes, so a script can tell a wrong file from an inconclusive result. It checks the manifest's signature first, soSHA256SUMS.minisigmust be downloaded besideSHA256SUMS- A transient lock no longer aborts an update (#1006): the swap renames the install tree aside, and Windows denies renaming a directory while any other process holds a handle inside it — antivirus scanning a binary whose bytes just changed, the indexer, a just-exited child. One attempt saw only "Access is denied" and the whole update unwound. It now retries with a short backoff. It always failed safe, restoring the tree, but the only recourse was to run it again
Tooling
refresh_deploy.ps1rebuilds the toolchain and refreshesdeploy-<arch>withoutdevenv, which ships only with the full Visual Studio IDE. It never deletes the tree, refreshes the.oblset as well as the binaries, and records what it installed with a hash per file — because a deploy binary replaced by another machine's build reporting the same version string is invisible otherwise, and invalidates every measurement taken against itaudit_ratios.pyinventories the divisions in Objeck sources and shortlists the unguarded ones. Advisory rather than a gate, and it refuses to report anything until it has classified a set of embedded fixtures correctlyrefresh_deploy.ps1installs the native libraries it builds (#1008):-Nativebuilt the eleven native solutions and nothing copied the results into the deploy tree, so a stalelibobjk_diags.dllcarrying the oldVER_NUMoutlived a version bump and surfaced as two unrelated-looking LSP test failures. Each builtlibobjk_*.dllis now installed and recorded inDEPLOY_MANIFEST.txt- The native-staleness warning times off
version.h's commit, not its mtime (#1012): an mtime records when the file was last written on this machine, so a clone, a branch switch or acprestore reset it and all eight deployed natives read as stale. It falls back to the mtime only whileversion.his uncommitted — a bump in progress, which is the one case the warning exists for - Every shipped example is compiled on each change (#1017):
programs/deployships 31 programs and had no way to build them -- the only check that touched them verified an example was listed in the index, not that the listing worked.build_examples.pyreads the-liblineREADME.mdprints beside each one, so a wrong column fails the build rather than misleading the first person who copies it
[v2026.9.6] - 2026-09-19
A correctness release for the machine-learning library and for the two places Objeck runs code that are not obr: the REPL and the embedding API. System.ML returned wrong numbers on fractional data, grouped points into empty clusters, and stopped the program on shapes it could not combine rather than saying so. obi and embedded programs used HTTP/1.1 without mentioning it, and on ARM64 obi aborted at the end of every run. Two crashes that only ARM64 machines ever saw are fixed, one of them in the collector.
Bug Fixes
API.OpenAI's textRespondalways returnedNil, and every image call sent its request as text (#901): the text path posted the request and never read the reply, so the call was billed and answered with nothing. Separately, each image entry point passed its arguments in the text implementation's order, which is the only one they fit, so the picture was never sent; one of them also dropped the caller's tools. A CI check now compiles the library and follows every call an image entry point makesobiand the embedding API had neither HTTP/2 nor HTTP/3 (#897): both run code through the module build, a separate compilation of the VM sources with its own macros and link line, and it never got the flags or the librariesobrhas. Code evaluated in the REPL or embedded in a C++ program quietly used HTTP/1.1. Every CI leg now runs one program under both and fails if they disagreeobiaborted on Linux ARM64 with "double free or corruption" (#897): the new parity check found it, and the cause predated the check. The ARM64 makefiles built the REPL's module with-D_X64and its VM without-D_MODULE, so the two halves disagreed about the layout ofNativeCodeandStackProgram: teardown read one object's field as another's and handedfree()a pointer that was never on the heapobinever exited when its input ended (#917): piping a program into the REPL, or closing its input, left it running forever, because the input loops did not treat a failed read as end of input- A Nil element in a
String[]crashed the VM inside a native library (#896):APITools_GetStringsValuesbuilt a string from the null pointer it is handed for an unset element. ONNX session options and DeepLab labels reach it - ONNX refused an empty image instead of crashing (#908)
- The compiler released a library file buffer with the wrong deallocator (#918):
~Libraryfreed withdelete[]memory thatmallochad provided. Harmless in practice, undefined behaviour under a sanitizer or a custom allocator Try()recovers from an invalid cast and from a full call stack (#900): both ended the program even inside a guarded chain, unlike every other runtime error.ObjInstCastprinted and exited without consulting the handler stack, andPushFrame's overflow path was a bare exit; a compiled callee's guard-stub status ended the program too. All three now recover when a handler is active, soa?->b()yieldsNiland the call stack is usable again. Inside JIT-compiled code both remain fatal: a failing cast is raised in a callback that cannot reach the handler stack, and compiled recursion overruns the native stack first- The collector could corrupt a class's closure-declaration map (#913):
StackClass::GetClosureDeclarationslooked the id up withoperator[], which inserts a default entry when the id is absent. The collector calls it from its marking threads for everyFUNC_PARMit walks, so two threads could insert into the samestd::mapat once and leave its tree inconsistent. Nothing failed at the time; the program crashed later at exit, in~StackClass, walking that map. It is a lookup now Matrix2D->SumColumnandAverageColumndropped fractions, soLinearSolver's R-squared was wrong on fractional data (#903): both started their accumulator as anInt(sum := 0;), so every value added was truncated --SumColumnof[1.25, 2.5]returned 3, not 3.75.LinearSolvertakes the mean of y fromAverageColumn, so a fit whose true R-squared is 0.9 reported 0.924. Both accumulators areFloatnow;ml_column_sum_fractions.obscovers the sums and the R-squaredKMeans->Groupreturned empty groups, andGetDunnIndexthen divided by zero (#903):Groupfiled each record under the label of its nearest centroid, but rebuilt centroid i from the i-th entry of its groups hash table, whose order need not match the labels'. When it did not, the centroids traded places on every pass and never converged, and a pass that moved them also cleared every group, so reaching the iteration cap returned them all empty -- four points in two tight pairs grouped 2/2 with the labels["a", "b"]and 0/0 with["b", "a"], and the three-cluster sampleprograms/tests/prgm301.obscrashed reading an empty group. Each centroid now follows its own label, and every pass reassigns the records from the current centroids.GetDunnIndexdivides the smallest distance between groups by the largest within one, which is 0 when every group is empty or has a single record; it returns 0.0 there now, so a degenerate grouping never wins a comparison between clusterings.ml_kmeans_groups.obscovers both label orders, a one-iteration cap and both degenerate casesMatrix2Doperations faulted on operands they could not combine, instead of returningNil(#903): the native matrix code rejects a shape mismatch or an empty matrix by setting the result slot itself toNil, and all 14 native-backed wrappers read their result back through that slot, soMatrix2D->DotProduct([[1.0, 2.0]], [[3.0, 4.0]])died dereferencingNil. Each wrapper now keeps its own reference to the result holder, and returnsNil. ANiloperand got past the two-matrix wrappers'm1 <> Nil | m2 <> Nilguard and crashed the native code with an access violation; the guard is&now, and the native code treats aNilmatrix as empty.Inverseof a non-square matrix never returned, because the native code ran Eigen'sinverse()before checking the shape; it returnsNilnow.NeuralNetwork->TrainreturnsNilwhen an input or target is not a column of the right height, where it used to fault inside the matrix code, andConfidenceof a wrong-shaped query is 0.0.ml_matrix_shape_mismatch.obscovers each caseMatrixReader's documentation contradicted its code (#903):target_offsetis the number of target columns, taken from the end of each row, not the index of the first one;LoadMatricesreads only CRLF-terminated rows; and a header row is read as a record. The documentation says so now, andml_matrix_reader_columns.obspins the column split for both loaders- Both
NeuralNetworksamples could not load their CSV, and faulted instead of saying so (#916):MatrixReaderreads rows throughCsvTable, which splits on CRLF unless handed another ending, andLoadSplitMatricesdefaults to CRLF.programs/deploy/neural_21.obs-- which is what a release ships -- andprograms/frameworks/ml/nn6.obsboth took that default while readingdata/gender.csv, LF-terminated since the repository's line endings were normalized in April, so the load returnedNiland the next line died with ">>> Attempting to dereference a 'Nil' memory instance <<<". Nothing in CI compiles or runsneural_21.obs, which is how it stayed broken. Both pass the LF ending now. Each also had aNilcheck placed after the first use of the value it was checking, so it could never fire; it runs right after the load and names the file it could not read.ml_matrix_reader_line_endings.obspins which ending each entry point accepts:LoadSplitMatricesreads an LF file when given one, returnsNilon that same file under its CRLF default, andLoadMatrices, which takes no line ending, cannot read it at all
Infrastructure
- A stress test for the collector's closure path (#920): the race behind #913 was found by an incidental test, and two plausible-looking attempts at a deliberate one missed the path entirely. This one reaches it about 16,000 times a run with 24 ids the class map has never seen, and runs at two nursery sizes, interpreted and with every method compiled, on both ARM64 legs of every PR
- The offline updater suite grew from 57 to 78 checks (#930): version ordering, payload integrity and interrupted updates, all without a network
- The Coverity scan scripts refuse a capture that would be reported as falsely clean (#905):
cov_scan.cmdhad not got past its version step since the day it was committed. Ten literal form-feed bytes sat where\fbelonged, so everyfindstrpath named a file that does not exist;cmd /cquote-stripping insideFOR /Fthen broke the version extraction and the unit count, and a count stuck at 0 means "empty capture, refuse", so every good capture would have been rejected. A crashed upload read as success, becauseif errorlevel 1does not see a negative exit status. Both scripts now compare each step's status with 0, andCOVERITY_DRY_RUN=1stops after the capture, so it can be checked before a scan is spent on it - Build junk and per-user IDE files removed from the repository (#898): 31 committed files and about 132k lines -- MSBuild logs and
.recipeintermediates, threeobc -asmdumps, Xcode UI state -- with.gitignorerules so they do not come back. The four.vcxproj.userfiles that hold debugger settings and the two Xcode breakpoint lists are kept on purpose. In a Windows checkout the untracked build output drops from about 230 files to 4; nothing that builds or ships changed
Documentation
- About 40 stale documents brought in line with the code (#902): every changed claim was checked against the source, the workflows or the live site, and the code examples were compile-checked.
SIGNING.mdandrelease_process.mddescribe the eToken flow rather than the retired CI signing;CI_CD.mdhas the real build matrix;architecture.md§10 describes what exists, including that there are no exceptions and thatTry()/?->recovery goes through a 16-deep handler stack; and the ONNX execution providers are documented as they ship, per platform. The pass also turned up #899: a library built with-tar libstops optimizing after dead-block elimination - The website's AI guide runs (#904): none of its nine machine-learning examples worked. All nine now compile with
-lib ml,gen_collect,csv---lib mlalone cannot resolveData.CSV.CsvTable-- and run to exit 0 both under the JIT and with--jit=off, with the expected-output comments matching what they print. Eight raw generics written unescaped (Pair->New(...)<String, String>) were being read as unknown tags and dropped by browsers. The page is hand-uploaded to objeck.org, so it needs one upload. Three of the library bugs fixed in this release were found while writing it - The native-interface store rule now names the real hazard (#907): it prohibited storing an object the caller gave you into an array. Both halves were narrower than the hazard -- a library's own allocations are old-generation as well, and a callback's result, or anything read out of a caller's object, is young -- so it reads: never store an object you did not allocate into an array, or into an object you did allocate. The #864 audit classified all 356 object stores in the shipped native libraries against this rule and none breaks it, so this documents current practice rather than changing it
[v2026.9.5] - 2026-09-19
Linux ARM64 users: v2026.9.4's binaries stop with "Illegal instruction" on most ARM64 CPUs (anything without SVE: Raspberry Pi 4/5, Graviton2, Snapdragon X, Apple silicon in a Linux VM). This release fixes that. Beyond it, a correctness and hardening release across the compiler, the garbage collector and the roughest corners of the language. Almost none of it was found by running programs: it was found by a heap verifier that checks the generational collector's invariants inside stop-the-world, a differential runner that holds every regression test to the same output at -opt s0 and -opt s3 under the interpreter and both JIT settings, and a program fuzzer that compares the same generated program across those configurations. Lambdas and enums account for most of the compiler fixes. Int arithmetic now has one definition shared by the interpreter, obc's folder and both JITs, which changes what >>> returns for a shift count outside 0-63.
New Features
System.ArraySortsorts primitive arrays in place (#887):ArraySort->Sort(a)andArraySort->Sort(a, low, high)forByte[],Char[],Int[]andFloat[].Int->Sort(v)and its counterparts still return a sorted copy and now share this implementation. It recurses into the smaller partition and loops on the larger, so its depth is bounded by log2 of the size, and switches to insertion sort below 16 elements: sorting a million random Ints went from 89.4 ms to 75.5 ms on an Apple M4 Max.Byte->SortandChar->Sortare nowpublic, likeInt->SortandFloat->Sort; before, user code could not call them
Bug Fixes
- Linux ARM64 binaries stopped with "Illegal instruction" on most ARM64 CPUs (#893): every Linux ARM64 makefile compiled with
-mcpu=native, and CI and the release build that platform on a server CPU with SVE2, so v2026.9.4's tarball required SVE and died at the first instruction of the interpreter's dispatch (cnth),--jit=offincluded, on a Raspberry Pi 4 or 5, AWS Graviton2, Ampere Altra, Snapdragon X under WSL2 and Apple silicon in a Linux VM. The build now targets ARMv8-A, which every ARM64 Linux CPU implements, with-moutline-atomicsso atomics still use LSE where the CPU has it;make ARM64_CPU=-mcpu=nativegives a machine-tuned local build. Measured on the build runner, six CLBG benchmarks moved by between -1.0% and +0.2%. CI now runsobcandobron an emulated Cortex-A72 (ARMv8.0, no SVE) with the JIT off and on, so a newer instruction fails the build instead of shipping INT64_MIN / -1stopped the program with no message (#828): the interpreter,obc's constant folder and both JIT folders each did the arithmetic in C++, which is undefined for overflow and for shift counts of 64 or more and faults on x64 --0xC0000095, in all four-opt s0/s3x--jit=off/--jit=1modes.core/shared/int_ops.hnow defines Objeck's Int arithmetic once -- wrapping+ - *, shift counts taken modulo 64,/and%truncating toward zero withINT64_MIN / -1 = INT64_MINandINT64_MIN % -1 = 0, and a zero divisor as the only trap -- and every site calls it. AMD64'sdiv_imm_regemitsnegorxorfor a literal-1divisor, and the register and memory forms branch aroundidiv;obcfolds both cases instead of leaving them to run time.int_semantics.obschecks each rule with literal operands, a runtime value against a literal, and runtime operands on both sides>>>returned 0 for a shift count outside 0-63 (#828):Int->ShiftRightUnsignedsaturated where<<and>>mask. It masks the count with 63 like every other Int shift, so-1 >>> 64is-1,-1 >>> 100is-1 >>> 36, and-1 >>> -1is 1.unsigned_ops.obsasserted the old saturation and follows the new rule. On ARM64 the immediate shift encoders tookabs()of the count instead of masking, sox << -1was encoded asx << 1and a count past 63 overwrote opcode bits; AMD64's shift immediates mask with 63- A divide by zero reported differently depending on how it ran (#828): the JIT paths printed ">>> Divide by zero ... in native JIT code" or "... in JIT-to-JIT call". Every path now prints exactly
>>> Divide by zero <<< obccrashed on a bare lambda with parameters and on a lambda nested in a lambda (#834):ParseLambdascanned the parameter list for a trailing~past the scanner's three-token look-ahead, soGetToken()returned nullptr; andBuildLambdaFunctionnulled the capture state after analyzing a nested body, so the enclosing lambda's next capture dereferenced null. The scanner now tokenizes past its buffer on request and restores itself, so a parameter list of any length classifies correctly -- a fixed 16-token window still rejected a bare lambda of ten parameters with "Expected '~'" -- and the capture state is saved and restored. Two error messages that pointed nowhere are replaced: a bare lambda whose type cannot be inferred (a ternary branch, one of several call arguments) says so and suggestsFuncRef->New(...)<R>instead of "Invalid alias" or "Invalid lambda type", and a lambda inside"{$ ... }"says lambdas are not allowed in string interpolation instead of "Invalid escaped string literal"- A lambda could not call a method on a captured object or call a captured function (#836): a lambda is compiled as a static function, so
InvalidStaticrejected an instance method called on a captured object with "Cannot reference an instance method from this context", and calling a capturedFuncRefor function reference failed with "Undefined function/method call" because the functional-call lookup never consulted the enclosing scope. Both receiver checks accept closure entries, and the name is resolved as a capture first. An instance method called with no receiver inside a lambda is still a compile error - A lambda nested in a lambda could not use a variable the enclosing lambda captured:
obcreported "Invalid operation using classes: Var and Int", or "Dimension size mismatch or uninitialized type", even forp[0]. A capture was resolved only in the symbol table of the method the lambda is written in; when that method is itself a lambda body the variable lives in the enclosing lambda's closure copy, so the lookup missed and the nested lambda declared the name as a new untypedVarlocal. Capture resolution now walks outward through every enclosing lambda, adding the closure copy to each intermediate one that does not have it - A lambda indexing a captured array by a variable or an expression crashed
obc(0xC0000005): the lambda-capture branch bound the variable to its closure-copy entry but, unlike the explicit-entry branch, never ran the dimension check or analyzed the indices, so the index variable had no symbol entry or type and the emitter dereferenced it. Literal indexes were unaffected, which is whyp[0]worked andp[i]did not eachover an array captured by a lambda would not compile: "Cannot reference an instance variable from this context", for every element type, while the equivalent index loop over the same array compiled and ran. A captured variable resolves to a closure-copy entry that is deliberately neither local nor static, andeachhands the collection's entry to the analyzer directly, taking the explicit-entry path whoseInvalidStatic(SymbolEntry*)check did not exempt closure entries as theMethodCalloverloads already did- A lambda capturing an array parameter --
Main'sargsamong them -- crashedobrin every mode (#849): a Nil dereference in the interpreter and0xC0000005under the JIT, forargs, any String or object array parameter and any String or object array local. A lambda resolves a captured receiver as a variable, and for a class-typed array that path never pushed the instance memory the array-entry path pushes, so the inlined$BaseArraySize()popped one value too many. Int, Float, Byte, Char and Bool arrays were unaffected - A call used as a statement inside a lambda body crashed the interpreter (#850): the lambda body was analyzed with the enclosing statement's
in_return/in_assignment/expression-depth state still set, so every standalone call in the body looked like a used value and noPOPwas emitted. The lambda returned with its operand stack unbalanced --0xC0000005at-opt s0ands3under--jit=off, hidden by the JIT's per-call stack - A typed lambda with a function-typed parameter would not compile:
\((Int) ~ Int, Int) ~ Int : (fn, a) => fn(a)failed with "Undefined function/method call: '#{L0}#(..)'". The lambda's generated method is found through a function reference that names each parameter by its type, and a parsed(Int) ~ Inthas no name, so the parameter was dropped from the lookup key; the encoded form is passed instead. Three defects on the same path came with it: the type parser took the parameters between the last(and the first)so several function-typed parameters parsed as one and the call failed with a size mismatch (both parsers now match parentheses); a nameless nested function type was encoded without itsm.prefix, so a call through a variable of type((Int) ~ Int, Int) ~ Intwas reported undefined; and theDYN_MTHD_CALLoperand counted parameters where the JIT reads operand-stack words, and a function-typed argument is two, so such a call crashed under--jit=1 - A program declaring a method whose parameter is a nested function type would not load: the VM's signature and reflection parsers skipped an encoded function type by scanning to the first
~and then the next,, which for((Int) ~ Int, Int) ~ Intstopped inside the inner type and hit): ">>> load error: Invalid method signature! <<<", even if the method was never called. Both parsers follow the parameter list to its matching)and recurse into a function-typed return - An enum or consts value could not be put in a string (#835): it could be printed with
e->PrintLine(), but"{$e}"and"x=" + efailed with "Class/enum variable does not have a 'ToString' method" and"{$C->R}"with "Expression type does not have 'ToString' method". The string paths, format specifiers such as"{$e:05}"included, now treat any enum or consts type as Int and append the integer value; consts members keep their consts type everywhere else, so library APIs that take consts-typed parameters are unchanged - Chained casts on an enum or consts value failed to compile or crashed at run time (#835):
e->As(Int)->PrintLine()failed with "Cannot cast a Nil return value" ande->As(Int)->ToString()with "Invalid cast with classes: System.Int and System.String", because the parser copies a receiver's cast onto the chained call and the analyzer then validated it against the call's return value; a scalar receiver cast is skipped there.x := e->As(Int)->Abs()compiled and crashed, because the emitter chose the receiver shape from the variable's class type and never pushed the scalar receiver slot.x := C->R->As(Int)->ToString()parsed the glued item nameC#Ras a variable, which the analyzer took for an undeclared local, so the call read no receiver.e->As(Float)reinterpreted the Int bits as a double --4.94066e-324-- where the enum item literal path already converted Size()andeachon an array of enum values saw Nil, and a field'sSize()returned garbage (#839): for a receiver typed as a program, inner or library enum array, the emitter loaded it like an object array and then pushed the scalar-enum instance load again -- twice for a library enum -- soSystem.$BaseArray:Sizesaw Nil. The scalar-enum loads apply only on the array-entry path now, which leaves every other receiver path's emitted code unchanged- An array of enum values captured by a closure read garbage under allocation pressure (#839):
a[i] := Color->Blueretyped the array entry as the scalar enum, so the frame, closure-capture and instance declarations marked the array slotINT_PARM. The collector never tracesINT_PARM, so the live array was freed, andc : Color := a[i]failed with "Dimension size mismatch or uninitialized type". Inner-enum arrays are declared as array types too - A method call on a string literal lost its receiver when an argument contained a call (#854):
"abcdef"->SubString(3, (3 + G->Zero()))crashed with an access violation and"abcdef"->Get(G->Zero())dereferenced Nil, at-opt s0ands3under--jit=offand--jit=1;"hello"->ReplaceAll("l", P->S())silently printed the wrong string. When the receiver is emitted before the arguments -- a string literal, an interpolated string, or the previous call in a chain -- an instance method needs each argument swapped beneath it, and the emitter keyed that swap on a counter only a string literal sets and every method call clears, so an argument containing a call dropped the swap and the call popped an argument as its instance. Chained receivers with arguments ("abc"->ToUpper()->SubString(1, 2)) never swapped at all. The swap is decided per call from the resolved method: for instance methods, not for the static helpers behind primitive receivers. A conditional receiver such as(t ? "abc" : "xy")->Size()crashed for a separate reason, fixed below (#863) - A
Bool[]field serialized to a different number of bytes on different runs:Bool->New[n]allocates a byte array, but everyBool[]local, parameter, field and closure capture was declared as an Int array. The collector marks both alike, but the serializer sized elements at 8 bytes and read past the byte block -- a holder of aBool[21]and anIntserialized to 129, 137, 120, 119 or 88 bytes on different runs, and 39 bytes every run with the fix -- and the heap verifier aborted every collection that found a liveBool[]on the declared memory type. Both declaration sites emit a byte-array declaration -opt s1and above dropped an assignment whose value had side effects: dead-store elimination removed an assignment to a local overwritten before it is read, together with every instruction of the statement, sov0 := s->M(p0); v0 := 1;dropped the call toMentirely; a constructor, a static function call, an integer division by zero and an out-of-bounds array read vanished the same way, and the last two stopped raising at-opt s3. A statement is removed only when every instruction of its value is effect-free. Found by a 5000-program differential fuzz campaign-opt s1and above could leave operands on the interpreter's operand stack (#843): a method returned its own value correctly and the caller's arithmetic then consumed the leftovers -- wrong digests, or a divide-by-zero error when a leftover 0 became the divisor. Dead-store elimination finds the start of a dead assignment by walking back over instructions of the same statement, and that walk stopped mid-expression on instructions spliced in by setter/getter inlining or made by constant propagation, which carried no statement; they now carry the replaced instruction's. Dead-code elimination and jump cleanup also dropped a conditional jump to the label right after it without popping its condition. The JIT's per-call stack hid all of it- A constructor call used as a statement leaked an operand-stack slot per execution (#848):
Foo->New(x);emitted noPOP_INTafter the constructor call, and a loop overflowed the stack --0xC0000005under--jit=offat-opt s0ands3 -opt s3hung or dereferenced Nil in a method holding a function-reference local (#844): the method inliner and LICM took the next free local slot from the declaration count, but a function-reference declaration occupies two slots, so the inlined temporaries of a call such asa0->Size()in a loop condition landed on the caller's last local -- the loop counter-opt s3inlining could put a live array in a slot the collector never traces: inlining added one to the id base for the callee's and/or temporary as well as the caller's, so whenever the caller had no temporary every inlined local landed one id above its declaration -- an inlinedInt[]or object sat in a slot declared Int, an Int in a slot declared a reference, and the last local fell past the declarations. The local space also counted the temporary's word as nothing, so the frame was undercounted and the interpreter, which zeroes only the counted words of a pooled frame, left declared reference slots holding a previous frame's words. Found by the heap verifier ingl_context_test- A method with many locals corrupted the heap when it ran interpreted (
0xC0000374): the compiler bounds a method's local space byLOCAL_SIZE, but the interpreter stores local id n atmem[n + 1], and a method holding the and/or temporary -- and/or, ternary, select,Try/Otherwise, or creating a lambda -- starts its locals at id 1. Frames were allocated at exactlyLOCAL_SIZE, so a 96-slot method wrote two words past the buffer. The frame buffer gets the two header words every other consumer already accounts for, and LICM no longer adds a cached slot that would take a method past the limit after the compiler's check - A capturing lambda stored in a local or an instance field crashed on its first call (#845, #847):
0xC0000005under the JIT, with no GC involved. Both JIT backends chose the two-word function-reference store from the first word's working-stack shape and assumed the second word had the same one. A closure built in a compiled method arrives as a closure-memory load under an immediate method id, so the immediate case emittedmovq $-120, -144(%rbp): the closure slot's own frame offset stored as the closure pointer, which the lambda's first call then read its capture through. Each word is stored by its own shape - Plain integer and float code at
-opt s3crashed under the JIT (#846): AMD64'sand,orandxorwith a memory operand built the REX prefix with the register and base swapped, unlike every other memory encoder. With a destination register in R8-R15 the REX.R bit was lost, so the result was written to RAX-RDI, and REX.B was set on the RBP base, so the operand was read through R13. With a loop local pinned in R13,v0 xor v0read an address well outside the frame and crashed; without a pin the same bytes silently read through the caller's R13. ARM64 loads the slot into a scratch register first and was not affected --jit=1read a field off the stack where--jit=offreported a Nil dereference: at-opt s3the inliner turnss->Get()into a field load throughsand constant propagation forwardss : Sh := Nilas a literal, and the JIT's load, store and copy paths treated any non-register object operand as a frame slot and loaded[RBP+0], the saved frame pointer -- so the Nil check passed and the field was read off the stack, and the program exited 0. A shared helper materializes register, memory and immediate object operands on both backends- A stack trace mangled a function-typed parameter: the formatter found the end of such a parameter by counting every
m.(in the whole signature and skipping that many~, then re-split the nested list at the last(and the first), so a method with one function-typed parameter unwound asP1->Crash(a:(a:Int, , , b:Int, c:Int, , , d:Int)~Int)and a one-dimensional array in the middle of a list printed asFloat[,]. Parameters, types and function types are read by recursive descent now, so traces readd:(Int)~Int, e:((Int)~Int, Int)~Int, g:(Int)~(Int)~Int. The debugger's frame names, in the CLI and in DAPstackTrace, use the same formatter - A young object reachable only through a closure stored in an older object was freed (#838): a closure's capture block always lives in the old generation, and the minor collection descended into a dirty holder's closure only when the closure itself was young -- which it never is -- while the capture store's own write barrier dirties an untyped block the dirty scan cannot read. The collection now descends into the closure's declarations regardless of generation. A fixture storing closures that capture new objects into promoted holders reported 253 of 256 rounds bad on v2026.9.4, in all four
-opt s0/s3x--jit=off/--jit=1modes - Strings in an array literal passed straight into a method were freed (#838): the trap that copies a string array literal writes freshly allocated young Strings into an array the allocator placed in the old generation, with no write barrier. While the array lives only on the operand stack or as a callee parameter, a minor collection neither repairs its elements nor, before the callee is entered, marks them at all, so they dangled once the nursery was recycled. The fixture reported 64 bad elements on v2026.9.4 in all four modes
- Objects were allocated at twice their field size (#840): the allocator, the young-object-start test and the AMD64 JIT's inline allocator all reserved
size * 2 + header. The doubling dates from the original 4-byte-int import and went stale when field slots became 8 bytes, so every object carried an unused slack half. All three now usesize + header, in step with each other, since the young allocation writes that size into the block and the start test compares against it. Before the change, a private build canaried the slack half of every object and checked every young object at each collection, every old object before each sweep and everything at exit, with the full regression suite reporting no violation under--jit=offand--jit=1 - Two objects could be lost at the very end of the nursery: with
--nurseryat or below about 300k a layout fixture failed deterministically, from two independent faults that both need a collection to land right after a particular allocation. A zero-field object had no field word, so its address was one past its own block; as the last allocation before a collection that address equalled the nursery's end, which every half-open young test rejects, so it was never marked, promoted or forwarded and the reset nursery reused its memory. And a closure's capture block, written through the barrier before its holder exists and held only by a closure slot and the operand stack, had the captured object promoted through its own local while the block's copy was never forwarded. Zero-field objects are padded to one field word everywhere the size is computed, and a dirty capture block is scanned conservatively -- marking only validated young object starts, relocating only genuine forwarding addresses - A stale or interior word from a conservative stack scan could be read as an object: the class lookup returned a nursery word's size-or-class slot as a class pointer whenever its type slot happened to read as Nil, and the young-object-start test then dereferenced it. The collector builds a sorted set of the program's real class pointers at startup and both sites check membership before use, and a young candidate must now be at least four words into the nursery, since callers read below it. Keeping an interior address on the operand stack across minor collections gave
0xC0000005on v2026.9.4 in all four modes - Running out of memory during a collection left dangling references or unwound from inside stop-the-world: a failed promotion allocation skipped the copy, so every reference to that survivor dangled once the nursery reset, and the old-generation insert, the promoted-object list and the frame list in root fixup could each throw out of stop-the-world with both collector locks held -- reported as ">>> virtual machine: out of memory". Both are fatal now: the collector prints "garbage collector: out of memory promoting a live object" and exits 1. The report is written with
fwprintf, because the wide-stream formatting itself threw under a real memory cap - A frame's
selfcould be overwritten with 1 during a collection (#820): all four frame fixup paths, three interpreter and one JIT, relocatedselfwith a raw read of the object's mark word. That word holds a forwarding address only after promotion has copied the object; before that it holds the mark bits, so a non-forwardedselfwith a non-zero word was overwritten. All four call one helper that relocates only through an address the forwarding test accepts -- aligned, inside the nursery, and a member of the old generation. This is hardening; the crash it resembles was fixed in v2026.9.3 (#821) - A multithreaded program could die with SIGSEGV and no output as threads exited (#861): when a thread's entry method returns, both exit paths release the top frame and store nullptr but leave the call-stack position at 0; the thread then unregisters as a mutator, so a collection on another thread stops waiting for it, while its frame monitor stays registered until the interpreter is destroyed several statements later. A collection landing in that window reached the root scan with a null current frame and dereferenced it on a mark thread. Root fixup and the heap verifier already skipped a null frame; the mark phase does now. It takes a small nursery, which makes collections near-continuous, and few cores, which keep the exiting thread descheduled: 7 SIGSEGVs in 2200 runs of the new fixture pinned to two CPUs on Linux x64 before, 0 in 2700 after. Three smaller changes harden the same handshake -- mutators join and leave the mutator set under the stop-the-world lock, instead of a store/load pair that release/acquire ordering does not make sequentially consistent; the interpreter prologue publishes the first frame before the call-stack position; and the frame slot is nulled before the monitor is registered
- A method call on a conditional receiver crashed with no output (#863):
(t ? 3 : 4)->Max(1),(t ? 'a' : 'b')->ToString()and(t ? "abc" : "xy")->Size()->Max(1)died with SIGSEGV at-opt s0ands3under--jit=offand--jit=1. The emitter treated every call hanging off a conditional receiver as nested, so a static$Int,$Charor$BaseArrayhelper lost the instance-memory word it pops. Only instance calls on a conditional receiver are treated as nested now.cond_expr_receiver.obsmakes 33 checks - A conditional over two arrays was typed as a scalar and returned garbage (#867):
(t ? p : q)->Size()over two primitive arrays resolved to the scalar helper and returned a meaningless value with no error, andx := t ? p : q; x[0]was rejected. A conditional's dimension now comes from the value each branch yields, so an indexed elementp[1]and a call such asp->Size()stay scalar, and branches of different dimension, such ast ? intArray : 5, are rejected with'?' invalid type mismatch; Nil stays assignable to either kind.cond_expr_array_type.obscovers six element types and keeps an array alive only through a conditional-typed local across a minor collection - A variable captured by a lambda could not be interpolated into a string (#865):
"n={$n}"on a capturednfailed with "Undefined variable" and warned thatnwas unreferenced, while"n=" + ncompiled. A lambda's closure copy of a captured variable is created on its first reference and registered where a direct symbol lookup never looks, so the interpolation path missed a copy that already existed, and missed outright when it was the reference that should create it. Interpolation now resolves the name as a capture first, through the same lookup-then-create path as any other variable reference eachover aRangecaptured by a lambda would not compile, and fixing that exposed a frame-slot clobber (#866): the loop did not recognize a captured range and reported "Undefined function/method call: Size(..)", for the same lookup reason as the interpolation fault above. Once recognized, the loop used the variable's own id as the frame slot holding the range, but a closure copy's id indexes closure memory, so the loop overwrote an unrelated local of the lambda -- seen as a Nil dereference. A captured range gets its own frame local; a local range variable keeps its slot, and all 290 existing regression tests compile to identical bytecode.closure_capture_resolution.obscovers both sites, every interpolated type, captured parameters, capture through two lambdas,IntRange,CharRangeandFloatRange, and a minor collection between building each closure and calling it- Sorting a primitive array could end the program (#887):
Int->Sort,Float->Sort,Char->SortandByte->Sortpartitioned on the middle element and recursed into both sides, so recursion depth followed the input. The VM's call stack is 256 frames, and a 600-element permutation whose middle element is the smallest of every range it lands in drove it 599 deep: ">>> call stack bounds have been exceeded! <<<" and exit 1, on every platform, at-opt s0ands3, interpreted and JIT-compiled. The same shape, without even a bounds guard, sorted user data inml_tree's split values andml_neighbors' KD-tree build. All of them now bound their depth;sort_primitive_arrays.obsandml_sort_depth.obscarry adversarial inputs that failed before in all four configurations - A runtime error or
Runtime->Exitcrashed the VM while another thread was running (#877): the program printed its message and then died with SIGSEGV, exit 139 instead of 1 or the requested status, on Linux x64, Linux arm64 and Windows x64 in both JIT modes, whenever one other thread was busy.exit()runs the process's teardown on the calling thread while the others keep executing: static destructors freed the collector's old-generation set under a thread allocating into it, and on Windows the static C++ runtime freed locale state under a thread formatting a number. While another thread is registered, the VM now flushes its streams and ends the process without teardown; single-threaded programs exit exactly as before.exit_with_live_threads.obscallsRuntime->Exit(0)from a thread while two others work, since a negative test would accept the crash - A thread join could wait on a handle that was never a thread (#874):
Joincopied theThreadobject's address into a local, parked the thread so collections could proceed, and only then read the handle out of it. A minor collection in that window promoted the object and reset the nursery, the local then named whatever reused that memory, andpthread_joinwas handed a small integer: ">>> Unable to join thread! <<<",ESRCH, once in a few hundred runs ofgc_mt_small_nursery_stresson macOS arm64 CI. The handle is now read before parking, in the interpreter and the JIT. A forced-window probe that failed 24 of 24 runs before the fix failed 0 of 24 after - A closure's captured values could be freed while still reachable (#881): a typed function-reference root traced a closure's captures only when its own mark set the capture block's mark bit. A conservative scan of a stale stack word could mark the same block first without tracing it, and the JIT-roots scan runs on its own thread beside the interpreter-frame walk, so when it won the race a capture reachable only through the block was swept. Exactly one typed walker now traces each block per collection, whoever marked it. The heap verifier found it on linux-arm64, whose JIT scans the words below the frame; forcing the conservative scan first reproduced it on every run before the fix and on none after
Performance
Data.CSVmedians sort aFloat[]in place (#892):CsvRow->MedianandCsvColumn->Medianboxed every value into aFloatRefand sorted aCompareVector; they now copy into aFloat[]and sort it withArraySort. 50 medians of a 20,000-value column went from 1.48-1.90 s to 0.13-0.29 s on Windows x64, with the same result every run.csv_median.obsis the first test to cover either method
Infrastructure
- A heap verifier for the generational collector (#832):
OBJECK_GC_VERIFYchecks the collector's invariants inside stop-the-world -- remembered-set membership before a collection, object headers, typed edges, the absence of typed nursery references and typed frame slots after it, unforwarded young objects during fixup, and withOBJECK_GC_VERIFY=checkmarka typed trace around the minor mark phase. Cheap checks run every collection and the full old-generation walk runs for the first 8 and then every Nth, backing off when verification exceeds about 20% of wall time. Only typed references are trusted: never operand stacks, JIT temp windows or pending-thread roots.OBJECK_GC_VERIFY_INJECTplants a corrupt promoted field, a skipped write barrier or a mark bit ORed into a young object's field, so the verifier can be shown to fail, and reports name the invariant, object, class, field, value and collection. With the variable unset each hook is one predictable branch. It found the inliner slot defect and theBool[]declaration above - Every regression test must print the same thing in every VM mode (#829):
run_differential.pycompiles each runtime test at-opt s0ands3and runss0/off,s3/off,s3/defaultands3/jit1, requiring identical stdout and the same zero/non-zero exit status. A first run on v2026.9.4 found 7 divergent tests, none of them a VM bug: timings, spin counts and GC and entry counts moved to stderr, and the two that differ by design carry markers.check_diff_markers.pyrequires a reason after each coverage-reducing marker and caps non-deterministic-output opt-outs at 5% of the suite - A differential program fuzzer (#837):
tools/fuzz/gen.pygenerates deterministic Objeck programs from a recorded choice stream over swarm-sampled feature layers,run_fuzz.pycomparess0/offagainsts3/off,s3/default,s3/jit1ands0/jit1, andreduce.pyshrinks a finding's choice sequence so every candidate stays a valid program. Afaults/directory holds a deliberately brokenobcandobrused to prove findings are caught. It found the-opt s3function-reference slot miscompile (#844), the interpreter-only dead-store divergence (#843), the zero-argument lambda JIT crash (#845) and the AMD64 REX-prefix crash (#846). JIT coverage is measured from positive evidence -- the VM reports each method it compiles, and each compile with inlined callees on AMD64 -- because subtracting rejected methods from the total scored a VM that ignored--jit=1at 100% - An EMI mode for the fuzzer:
tools/fuzz/emi.pymutates regression tests that compile and exit 0 -- inserting dead code, deleting unexecuted blocks, applying runtime identities -- into variants that must print the original'ss0/offoutput, and compiles and runs each at-opt s0ands3under--jit=offand--jit=1. Guards read a static set fromargsso-opt s3cannot fold them, and unexecuted blocks are found with a stderr probe build rather than coverage tooling. A variant whose configurations agree with each other but not with the original counts as a divergence, which is what catches a front-end miscompile that is wrong everywhere; it found the string-literal receiver defect (#854) - A nursery size knob and GC exit statistics (#831):
--nursery=<n>(k|m), orOBJECK_NURSERYwith the flag winning, sets the young-generation limit between 64k and 128m; anything else is an error naming the range. The full region stays reserved and only the limit both bump paths compare against moves, and an object larger than the limit goes straight to the old generation instead of forcing a collection per allocation.runtime.memory.peak,runtime.gc.nursery.capacityandruntime.gc.promoted.bytesare new, pauses are timed from taking the sweep lock until the world resumes so the stop-the-world handshake and the minor dirty-list scan are included, andOBJECK_GC_STATS=1prints one line to stderr at exit with minor and major counts, pause percentiles, promoted objects and bytes, and peak RSS. Small nurseries are what exposed the nursery-end losses and the thread-exit race above - A nightly hardening workflow (#833): on the five CI platforms, against the latest green master artifacts -- differential regression, the fuzzer, the regression suite under the heap verifier in minor stress (a 256k nursery) and major stress (a 64k threshold), and GC stress loops in both JIT modes.
tools/cicd/nightly_triage.pywraps each step, fails on a missing tool or knob instead of skipping, and classifies results as infra, known or new; a killed step is a new failure, not infra. The first run reported 33 new signatures for what was two test-output bugs, one forced-threshold cost and a handful of real findings, so collection-timing counters moved to stderr, two opt-out markers were added with a lint that requires each one's reason to name the run whose log shows the failure, and the triage now opens an issue only for a failure new on more than one leg or one that is a crash or a verifier violation, listing the rest in a single tracking comment - A stress-probe workflow:
stress-probe.ymldownloads the platform trees from the latest successful build on a branch and loops ten GC and JIT fixtures on all five CI platforms in four modes -- default,--jit=1, a 256k nursery, and the heap verifier with a 256k nursery.tools/cicd/stress_probe.pycounts failures per fixture and mode, saves the output of the first three, fails on any failure or a missing VM knob, and writes a summary with the rule-of-three bound. Run counts are measured per cell. It found the thread-exit race (#861) - Negative regression tests name the error they expect (#830): the compile- and runtime-error markers count only at the start of a line in both runners and in the manifest generator, so a comment mentioning a marker no longer makes a test negative, and
# EXPECT_COMPILE_ERROR: <message>requires that message in the compiler output. Each of the 19 compile-error tests states a substring of the error it expects; one of them had been passing on the wrong error.OBJECK_VM_ARGSpasses extraobroptions to every test in both runners, andcheck_test_exit_codes.pyrejects marker lines the two runners cannot honour identically - A test that guards object layout against the allocation size:
obj_size_layout.obsbuilds the shapes most likely to disagree with a class's instance size -- a zero-field class, a Float last field, a function reference last field, a library-class parent, a closure capture held in a field, a serialization round trip with a trailing function reference, and a native crypto call storing into a holder -- keeps them alive through minor and major collections, and asserts that collections and promotions actually ran. It is what caught both nursery-end losses once objects lost their slack half - The macOS deploy builds the verifier, and stops when a toolchain build fails: the new verifier source was added to the Visual Studio projects and the arch makefiles but not to the Xcode projects macOS builds
obr,obdand the module library from, so all three failed to link -- and the deploy's "Build toolchain" step still ended green, because the script had noset -e; the missingobrsurfaced only in the smoke test. The Xcode projects compile it, and every toolchain build and product copy in the macOS deploy exits on failure - The benchmark harness and the published numbers use the same inputs: the harness ran
binarytreesat depth 21 where every table indocs/performance.mddocuments 17, roughly a 16x difference in work, so any number it produced would have been silently incomparable to the page it was going into.tools/cicd/check_benchmark_inputs.pyparses the harness's benchmark arrays and every table that names one of them and fails when a shared benchmark carries different inputs. It also checks the benchmark set, so a benchmark on disk that the harness never runs and the documentation does not exclude is caught, and it runs in CI with its unit tests.perf-results/objeck_bench.pyis the cross-platform driver that produced this release's baselines: interleaved runs, run counts scaled so each benchmark gets at least 10 s per build, peak RSS on all three platforms with the source of each value recorded, and astressmode that refuses to run without CPU affinity rather than report a clean result that could not have failed - The thread-exit race loop oversubscribes where it cannot pin (#861): the race needs more runnable threads than CPUs. The VM flag tests pinned the fixture to two CPUs with
taskset, which macOS and Windows do not have, so there the loop ran sequentially and could not have failed at any run count.run_vm_flag_tests.pynow runsceil(cpus * 3 / 8)concurrent copies of the eight-thread fixture instead. On macOS arm64 that reproduced the race 11 times in 10,000 runs before the fix and 0 in 10,000 after - A failed thread join says why (#874): ">>> Unable to join thread! <<<" was printed with nothing else, once on macOS arm64 CI, and the return code that separates the causes was discarded. Both join paths, interpreter and JIT, print a second line with the
pthread_joinresult and itsstrerrortext -- on Windows, theWaitForSingleObjectresult andGetLastError-- plus the thread handle and theThreadobject's address. The first line is unchanged, so existing log matching still works - A test that prints a failure and then returns is caught before it is written:
tools/cicd/check_test_exit_codes.pyrequired a test that prints FAIL to contain an exit call somewhere.odbc_sqlite_testhad one and was still silent: the branch that found no database printedFAIL: Cannot open connectionand returned, and a bare return from Main is exit 0, which both runners score as a pass, so on every machine without the SQLite ODBC data source it ran no check and counted green (#819 fixed that test). A FAIL print whose next statement is a barereturn;is now an offense, and the report names both remedies: exit non-zero when the branch is a failure, or print a line starting withSKIP:and return when the machine lacks a driver, data source or service, which both runners record as a skip. Tests marked# EXPECT_RUNTIME_ERRORare exempt, since the runner fails those on exit 0 core_odbcno longer reads as ODBC coverage: onlyConnection,ParameterStatementandResultSetcall intolibobjk_odbc, so a test ofDate,TimestampandColumnInfonever loads it, yet its header called it "Core ODBC Bindings Test".odbc_sqlite_testis the only test that calls into the binding, and its data source exists only on the two Linux legs- The heap verifier no longer reports a dead closure as a lost capture (#880): its B3 check assumed every surviving closure capture block was live, and reported one that still named a nursery object as a promoted capture left unforwarded. On linux-arm64 about 2% of verified runs of
closure_block_bodytripped it, and the cores showed a garbage block kept alive only by a stale JIT stack word, whose capture had been correctly freed. The capture's old header now decides: a forwarding address is still a violation, anything else is a warning. Reintroducing the original fixup bug still produces B3 violations in every affected configuration - The rule behind 37 allocation sites is written down: allocating can run a collection, which moves young objects and resets the nursery, so a GC pointer held in a C++ local goes stale across it. Nearly every trap allocates with collection disabled while such a pointer is live;
memory.hnow says so next to the allocators, and the one trap that did not,LoadMultiArySize, follows it. An audit of all 21 park and safepoint sites found them already safe
Documentation
- An Integer Arithmetic section: wrapping
+ - *, shift counts taken modulo 64, truncating/and%withINT64_MIN / -1 = INT64_MIN, and a zero divisor as the only trap - What a lambda can capture: that captures are copied when the lambda is created, that objects are therefore shared by reference, that a lambda may call methods on captured objects and call captured functions, and that
self,@fieldand receiver-less instance calls are not available in lambda bodies yet - Performance baselines in
perf-results/baseline-2026.10.0/: master against v2026.9.4 on macOS arm64, where no benchmark is more than 5% slower at p<0.01 andbench_gc_large_heap's peak RSS falls 26.6% with #840, and the #841 nursery-size sweep on macOS arm64, Linux x64 and Windows x64. The default nursery stays 128m: 64m cut peak RSS on macOS but madebinarytrees10.5% slower on Linux x64 and 25.8% slower on Windows x64, where promotion costs more (#871) docs/PLAN_2026_10_0_HARDENING.md, the hardening plan this release follows (#842)
[v2026.9.4] - 2026-09-15
A corrective release for the compiler: two optimizer rewrites computed some integer divisions wrong at the default optimization level, with no error. Programs compiled with v2026.9.3 or earlier at -opt s2 or -opt s3 (the default) should be recompiled.
Bug Fixes
- Integer division by a power of two rounded the wrong way at
-opt s2and-opt s3: strength reduction replacedn / 2^kon a local with an arithmetic right shift. A shift rounds toward negative infinity and integer division truncates toward zero, so a negative dividend that was not an exact multiple came out one too low:-7 / 2gave-4,-1 / 1024gave-1. Division is no longer rewritten; both JITs already compile a constant divisor to a biased shift or a multiply. Multiplication by a power of two is unaffected. The interpreter and both JITs agreed on the wrong value, so no mode comparison could see it;opt_int_division.obschecks hand-computed quotients at every level 1 / nevaluated tonat-opt s3: a peephole pattern meant forx / 1matchedLOAD_INT_LIT 1; DIV_INT, but the value pushed last is the left operand, so it removed the division from1 / ninstead. The pattern is gone- The compiler crashed folding
INT64_MIN / -1: constant folding at-opt s1and above divided the two literals in C++, which faults on x64 (0xC0000095), soobcexited with no message and no output file. The fold is skipped and the division is left to run time, as for a zero divisor - Deserializing an object array with a Nil element corrupted the enclosing object: the deserializer wrote each Nil element into the object's next field slot instead of into the array, so the array field itself read back as Nil and every later field shifted.
serial_nil_array_element.obsround-trips an object with Nil elements between its fields
[v2026.9.3] - 2026-09-14
Bug Fixes
- macOS:
obrcould not start on a user's Mac (#812): every macOS release since v2026.6.3 linked Homebrew's GnuTLS, nghttp2, nghttp3 and ngtcp2, plus an ngtcp2 GnuTLS backend that Homebrew does not ship, which the build machine had built by hand. On any other Macobrstopped at launch with "Library not loaded: @rpath/libngtcp2_crypto_gnutls.8.dylib", even after the release notes'brew install. HTTP/3's TLS moves from GnuTLS to AWS-LC, andobrlinks AWS-LC, ngtcp2, nghttp3 and nghttp2 statically on Linux and macOS, built bytools/deps/build_quic_deps.shfrom pinned versions. A Linux archive no longer needs those libraries from its distribution. Release builds install the macOS package on a clean runner, with Homebrew moved aside, before anything is published - macOS: the OpenCV, ONNX and LAME bindings needed Homebrew (#817): v2026.9.2's
libobjk_opencvandlibobjk_onnxlinked Homebrew'sopencv@4andonnxruntime, andlibobjk_lameitslame, so on a Mac without those formulas they failed with "Library not loaded". The package now carries every library it uses.tools/deps/build_macos_deps.shbuilds OpenCV 4.12.0, mbedTLS 3.6.4 and libiodbc 3.52.12 as static libraries and LAME 3.100 as a shared one (it is LGPL), and fetches Microsoft's ONNX Runtime 1.30.0, each pinned by version and SHA-256.libonnxruntime.1.dylibandlibmp3lame.0.dylibship inlib/native, and the five licenses indoc/licenses. The OpenCV and ONNX bindings build with CMake (core/lib/opencv/macos/CMakeLists.txt), and the vendored mbedTLS and libiodbc archives leave the repository. The package runs on macOS 13.3 or later, and the ONNX binding on macOS 14; the app'sLSMinimumSystemVersionhad said 11.0 - Linux and macOS: the libraries inside
obrshipped without their licenses (#818): since #812,obrcarries AWS-LC, ngtcp2, nghttp3 and nghttp2, and no archive included their license texts.tools/deps/build_quic_deps.shinstalls each library's license files from its pinned tag, and both deploys ship them indoc/licenses. Rebuilding the QUIC libraries no longer deletes the macOS bundled libraries that share their directory, and on macOS they target 13.3 whenMACOSX_DEPLOYMENT_TARGETis unset; a build by hand had produced archives for the build machine's own macOS - ODBC:
Connection->GetLastError()was empty for a connection that did not open (#819): a failed connect freed the handle that held the driver manager's diagnostic, so the reason -- a missing data source, a driver that would not load, a rejected login -- never reached the program. The binding keeps it, per thread, and returns it, e.g.[IM002] ... Data source name not found - ARM64: a collection could corrupt an object while compiled code ran on another thread (#821, #816): the collector marked every word its conservative scans handed it that pointed into the nursery, without checking that the word was the start of an object. A stale or interior pointer, which ARM64's compiled code leaves in the stack slots those scans read, had the mark bit set in whatever word lay below it. For a map's tree node, that turned an empty
@rightinto 1, and a later lookup crashed on it: silently or as "Invalid object cast" on Windows arm64, SIGSEGV on macOS and Linux arm64. It took compiled code, a second allocating thread and frequent collections; a map-insert loop built that way failed about 1 run in 6 on Linux arm64. The collector now marks a nursery address only when it is a real object's start - Linux:
install_deps.shoffered only SDL2 (#809): a Linux archive needs its distribution's runtime libraries -- mbedTLS, readline, OpenCV, LAME, ODBC and SDL2 among them -- and the script shipped inside it installed SDL2 alone, so the other bindings failed to load on a fresh machine. It now maps every library the binaries link to its package (ldd, soname, package) and installs the whole runtime;--checkreports what is missing
Infrastructure
- A regression test that did not run no longer passes (#819):
odbc_sqlite_testprinted "FAIL: Cannot open connection" and exited 0 wherever its SQLite data source was missing -- macOS, both Windows legs and most developer machines -- and both runners counted that as a pass. A missing data source is now a skip that names the reason, and any other connect failure fails.http_persistence_teststarts on a random port and waits for a real connect, instead of a fixed port and a fixed sleep - The macOS deploy holds every Mach-O file to macOS 13.3 (#817): a library built for a later macOS does not load on an earlier one. The deploy reads the
minosof every file in the tree and of every member of the static archives it links, and fails on one above 13.3, ONNX Runtime's 14.0 excepted; an archive built for a later macOS links with only a warning, and the binary still reports 13.3. Every build is given the deployment target and the dependency prefix explicitly.create_macos_pkg.shcreates itsOUTPUT_DIR - macOS bindings carry
@rpathinstall names, and the deploy checks them (#811): v2026.9.2's bindings shipped with whatever install name their builds set, two of them the CI runner's build directory andlibobjk_ml's an Xcode template's/usr/local/lib/libxcode.dylib; onlylibobjk_sdlhad been corrected. All eight are rewritten to@rpath/<name>and re-signed, and the deploy fails if one is not. Loading was never affected, since the VM opens a binding by absolute path - The macOS deploy checks where every library in the tree resolves (#815): v2026.9.2's
obrfound an ngtcp2 backend that Homebrew does not ship through its onlyLC_RPATH,/opt/homebrew/lib, and the deploy's checks since --obr's absolute links (#812) and the bindings' install names (#811) -- do not read rpaths.tools/cicd/check_macos_tree_links.shfollows everyLC_RPATH,@rpath,@loader_path,@executable_pathand absolute link of every Mach-O file in the finished tree, the app launcher included, and the deploy fails on one that resolves outside the tree or macOS. Now that those libraries ship in the package, nothing in the tree is allowed an outside link, in the deploy or in the install tests - Release Build dry runs cannot publish (#808): dispatching Release Build from master published a release, and its docs job pushed
api.zipto master from any ref. Publishing and theapi.zippush now happen only for a release tag, so a dispatched run is a safe dry run - One verification script per machine (#814):
tools/cicd/verify_platform.shandverify_platform.cmdbuild a clean checkout the way the release does, run the regression suite with the default JIT and with every method compiled plus the VM flag, debugger and DAP tests, and write a PASS/FAIL result file; a step that skips counts as a failure.tools/cicd/check_docs_deps.pyfails CI when install instructions or the shipped binaries disagree withcore/release/runtime_deps.json, and a failed master or release build opens a tracking issue http_error_bodyno longer flakes (#810): the test's own server wrote the error body and closed the socket at once, so the client sometimes read a reset instead of the body. It closes gracefully, listens on a random port and waits until the server is ready
[v2026.9.2] - 2026-09-13
A corrective release for v2026.9.1's native libraries: macOS ships OpenCV and ONNX again, Linux x64's ONNX loads without LD_LIBRARY_PATH, and Linux ARM64 no longer carries an x86-64 ONNX Runtime. Underneath all three, the deploys verified what they built and ignored the answer. They now stop instead of shipping incomplete -- on a missing native library, and on a missing file an example opens by name -- and the deploy scripts say what they are doing while they run.
Bug Fixes
- macOS:
libobjk_opencvandlibobjk_onnxmissing from v2026.9.1 (#804): Homebrew's defaultopencvformula is now OpenCV 5.0, which movedcontourArea,boundingRect,getRotationMatrix2D,approxPolyDP,estimateAffinePartial2DandLMEDSout ofcv::. CI had been pinned toopencv@4andrelease-build.ymlhad not, so the release built neither binding and published without them. The release workflow now installsopencv@4as well - Linux x64:
libobjk_onnx.soloaded only withLD_LIBRARY_PATH(#804): it was linked with no RUNPATH, so thelibonnxruntime.so.1shipped beside it inlib/nativewas never found unless the environment pointed there -- which CI's does and an untarred release's does not.core/lib/onnx/eq/build.shlinks with-Wl,-rpath,'$ORIGIN' - Linux ARM64: an x86-64 ONNX Runtime in the tarball (#804): only
cuda/lib/x64is vendored, so the ARM64 link failed anddeploy_posix.shstill copied the 16 MB x86-64libonnxruntimeinto the aarch64 tree. ONNX is built only for an architecture with a vendored runtime, and is declared optional on ARM64 gl_crystal.objnever shipped (#803, #805):gl_model.obsloads it by default and every deploy script copied it, but.gitignore's*.obj(meant for MSVC object files) kept the Wavefront model out of git, and no deploy checked a copy. Committed as a hand-written quartz crystal, with!programs/examples/*.objso the next model is not hidden the same wayImage->Normalize()could write past its output array (#806):opencv_normalizesized the float array asfloat_img.rows * float_img.cols * 3, computed in 32-bitintbefore widening tosize_t. Above about 716 megapixels the product wraps: a negative size (allocation failure) up to about 1.43 gigapixels, and a too-small array that the fill loop overruns -- an out-of-bounds write on the VM heap -- beyond that. The size is computed insize_t(CodeQL alert #362,cpp/integer-multiplication-cast-to-long)
Infrastructure
- Deploys act on native-library verification (#804):
verify_native_libs.shran on the POSIX deploys without|| exit 1, so v2026.9.1 logged "native-library verification failure(s)" on three platforms and the jobs stayed green. The result now fails the deploy; each platform declares its required libraries, and--optionalnames one a platform cannot build yet (reported as a warning and a GitHub annotation). CodeQL installslibopencv-devandlibmp3lame-dev, so the OpenCV, ONNX and LAME bindings are built and analyzed - Deploys fail on a missing example asset (#805):
core/release/verify_example_assets.sh(POSIX) and a matching check indeploy_windows.cmdfail whenREADME.md,data/gender.csv,data/weather.json,opengl/cube_gl.obs,opengl/gl_crystal.objoropengl/gl_model.obsdid not land. The MSYS2 UCRT and Clang deploys now copy the OpenGL examples, the examples README anddata/..gitignorere-includesprograms/deploy/data/, whichdata/hid at any depth - Deploy script presentation (#801):
ui.sh,ui.cmdandui.ps1givedeploy_posix.shanddeploy_windows.cmda banner, live stage progress and quiet tool output, with the tail of the log shown for a failed stage and-v/VERBOSE=1to stream everything. The live line reads correctly in VS Code and Windows Terminal -- one solid bar coloured by finished stages and a stage count, where a block-and-shade bar looked full at 6% and a percentage sat still through the multi-minute solution rebuild -- and falls back to ASCII in a plain console; its activity text names the project being built instead of MSVC's chatter - Generated-template guard (#802): the
search_index.jsonvalidation had been added only tocore/release/code_doc64.cmd, whichupdate_version.ps1regenerates fromcode_doc64.inon every Windows deploy, so it never ran. It is ported into the template, andtools/cicd/check_version_templates.pyfails CI when a generated file and its template disagree
Documentation
- The definitive v2026.9.1 benchmark run, with
fastameasured for the first time (#800) docs/ML_TABULAR_GAPS.md: what a tabular classification study needs fromSystem.ML(#798)
[v2026.9.1] - 2026-09-12
Compiled code now calls compiled code directly -- a bound call 26.5 ns to 5.5 ns, a virtual call 125 ns to 6.0 ns, Fib(32) 0.208 s to 0.043 s -- and three ways compiled code could corrupt memory are closed. Every string hash was wrong; JIT arithmetic now matches the interpreter, and the JIT is 2-8x faster on the loops programs actually write; the debugger sees threads and tells the truth about what its commands do; Game.OpenGL gains a sky, rotations, normal maps and emissive materials; and the release process reports what it actually did. Found by running the v2026.9.0 release and then surveying every binary and library for the bugs a compiler, VM and debugger platform cannot carry.
At a Glance
The largest entry in this changelog by some distance — 100 bullets across 26 issues and pull requests, against 42 for the next biggest (v2026.8.2) — and weighted heavily toward correctness rather than features.
| Area | Count | The short version |
|---|---|---|
| Bug fixes | 44 | Every string hash was wrong. Three ways compiled code could corrupt memory. Nine debugger commands reported success while doing the wrong thing. |
| Virtual machine | 25 | The F7 calling convention, jump tables for a dense select, checked VM flags, runtime properties and locales, POSIX exit codes. |
| Infrastructure | 11 | Libraries rebuilt by their own compiler, the every-method-compiled CI pass on all five legs, release gating that fails instead of skipping. |
| New features | 6 | Threads the debugger can see, Game.OpenGL sky/quaternions/normal maps/emissive materials. |
| Compiler | 6 | Short-circuit conditions, name resolution that stops copying the library maps, the func-ref inliner fix. |
| Testing | 3 | A guard on the headline performance number, and fixtures for the fixes above. |
Performance. A bound call went from 26.5 ns to 5.5 ns, a virtual call from 125 ns to 6.0 ns, and Fib(32) from 0.208 s to 0.043 s on AMD64 (17.1 → 7.4 ns and 0.135 → 0.050 s on an M4 Max). Measured end to end on the benchmark suite (docs/performance.md, 2026-09-12): spectralnorm 16.4x, binarytrees 3.9x, nbody 2.3x, and micro-benchmarks up to 53x. fannkuchredux and mandelbrot did not move.
The three that matter most if you are upgrading. Every Hash->SHA256, HMAC or signature Objeck computed over a string was wrong, because a Byte[] carried its element count plus one in the word native libraries read — no digest ever matched another implementation. A constant character stored into a Char[] overwrote its neighbours on ARM64 and Linux x64, including inside String->Pop() and String->Delete(). And a compiled frame holding a declared-but-unreferenced local was scanned by the collector under the wrong types.
New Features
- Breakpoints fire inside threads, and threads are real (#719) — a breakpoint in any thread's
Runnever fired: spawned interpreters were built with a null debugger pointer (the August fix for a crash on a thread's first instruction), so every spawned thread was invisible toobdand DAP answered everythreadsrequest with a fabricated single thread while claimingallThreadsStopped. Spawned interpreters now attach to the one debugger; the model is all-stop, which is what the stop state already assumed — a thread reaching the hook while another owns the stop waits its turn, and three threads on one breakpoint stop three times with their own frames. Each thread gets a stable small id and its last source position:obdgains athreadscommand, DAP reports the real list, and the stopped event names the thread that stopped. Conditional breakpoints evaluate against the stopping thread's frame (the fixture provesstep = 200is satisfiable only in worker 2). Three CLI tests in both harnesses, six DAP checks in a new suite Game.OpenGL:Skybox— a procedural sky evaluated per pixel from the view direction (horizon colour, zenith colour, a sun), so an outdoor scene gets a sky by saying what colour it is; no assets, no cubemap upload path, no native changeGame.OpenGL:Quaternion— a rotation as a value: slerp for orientation tracks (Euler lerp does not take the short way round and stalls when axes align) and composition for turret-on-hull accumulation; a headless test caught the first version being wrongGame.OpenGL: tangent-space normal maps —Material->SetNormalMapand three shader factories (LitTexturedNormalMapped,…Shadowed,…PointShadowed); the tangent frame is derived in the fragment shader from screen-space derivatives, so no vertex attribute, no native upload change, and OBJ meshes without tangents work unchangedGame.OpenGL:Material->SetEmissive— light a surface gives off itself, added after lighting rather than multiplied into it; the one way to make a surface visible where no light arrives (the lantern's relics, and a shade standing between the player and their own lamp)- The lantern demo is a game (#716 and the commits before it) — monsters that ignore the lamp, relics to find, a hide with relief, fuel that tells the truth about reach, a completable arc; the last relic pulses so a session does not end sweeping swept corners
Bug Fixes
- JIT on ARM64 and Linux x64: a constant character stored into a
Char[]overwrote the elements after it (#781) — the char-array store's constant case wrote 8 bytes: ARM64 stored the whole register (itsmove_imm_mem32went through the 64-bit store) and AMD64 outside Windows amovq(itsmove_imm_mem32wasmove_imm_mem). Soa[i] := 'x'also zeroed the next element on Linux and macOS (4-byte characters) and the next three on Windows ARM64 (2-byte characters), and wrote past the end of the array wheniwas the last index; Windows x64 stores 16 bits there and was never affected. A forward fill rewrites each clobbered element on its next store, so code that builds a string left to right came out right; a constant stored below an element that was already set destroyed it. Both helpers store 32 bits now and the ARM64 constant case stores 16 on Windows, as the register and memory cases always did. The Windows x64 native entry's four-byteipandjit_offsetwent through the same AMD64 helper and wrote over their neighbours, harmlessly (the next store resetsjit_called; the rest is padding); they are four-byte stores now too.jit_const_char_store.obsfails before the fix on linux-x64 and linux-arm64 (96 characters overwritten) Console->WriteBuffer(Char[])encoded its output twice, and corrupted every write after it (#793) —obrputs stdout in a wide CRT mode (_O_U8TEXTby default, or whatever--objeck-stdioselected) and the rest of the VM writes wide characters throughwcout, letting the CRT encode them.StdOutCharAryLendid the conversion itself and wrote the resulting bytes, so the data was UTF-8 encoded twice — once by the trap and once by the CRT, which reads those bytes back a pair at a time as onewchar_t.'A','B'arrived as U+4241 (䉁). The invalid sequence then left the stream unusable, so a single call corrupted the rest of the program's output, including ordinaryPrintLinecalls that followed. The trap also ignored itsnumargument:UnicodeToBytestakes a NUL-terminated pointer, so it wrote to the first NUL rather than the count requested. It now handswcoutthe wide characters and lets the stream's mode decide the encoding, as every other output path does. Verified on Windows and Linux, because the changed branch is shared and POSIX stdout is a plain byte stream where the old code was correctfastanever compiled, and had been silently skipped by the benchmark harness since at least June — four float literals carried adsuffix (0.3029549426680d), a Java-ism from the original shootout source; Objeck has only ever had ausuffix. The harness discarded the compiler's stderr, so the failure surfaced only asSKIP: fasta (compile failed). Fixing it exposed theWriteBufferbug above, which is whyfastais still absent from the v2026.9.1 performance tables- Windows x64 JIT: compiling a method that held ten integer values at once ended the process (#773) — Windows adds
RSIandRDIto the AMD64 allocator as auxiliary registers, handed out only when the eight pool registers are all live. Once one of them held a value that was stored to a local and then read back through the local-register cache, it sat in two of the compiler's lists at once, and the compiler deleted every list when it finished the method: one register holder was freed twice, Windows' heap reported corruption, andobrexited with0xC0000374, before the program had printed anything. The machine code was never wrong. The holders now belong to a single list and are deleted once; the two in-use lists, which nothing else read, are gone, and a failed compile no longer leaks its holders. Linux x64 has no auxiliary registers and ARM64 keeps its own lists, which is why only windows-x64 failed, and there only with every method compiled: the one test with such a method called it three times, below the auto-JIT threshold.jit_entry_shapes.obsgainsTenLive, a ten-term probe called often enough to compile in both regression passes, which ends the old VM in both obrexited at startup under a locale the C++ library could not construct — on macOS, settingLC_CTYPEwithoutLANGmakes the C library report the per-category compositeC/C.UTF-8/C/C/C/C, which libc++ cannot turn into astd::locale; the constructor threw out of the entry point and the VM printed>>> virtual machine: collate_byname<char>::collate_byname failed to construct for C/C.UTF-8/C/C/C/C <<<instead of running the program. That is exactly the environment Python 3 puts in every child whenLANGis unset (PEP 538), so every Python harness —run_vm_flag_tests.pyamong them, seven failures from any shell with noLANG— hit it, and the workaround wasLC_ALL=en_US.UTF-8. The localewcoutconverts through is now built by a helper that never throws: the namesetlocalereturned, then theLC_CTYPEcategory alone (the one that decides how wide characters are written, and the one the composite was hiding), thenC.UTF-8anden_US.UTF-8, then the classic locale with a UTF-8 converter, which needs no locale data at all. Wide output stays UTF-8 in every fallback, and the C library's own locale is left as the environment set it. glibc names its composites in a form libstdc++ accepts, so Linux takes the first candidate as before. The flag tests runobrunderLC_CTYPE=C.UTF-8alone and under a locale the system does not have, and check the bytes of a wide string (vm_locale_wide.obs)- A C++ exception under JIT-compiled code aborted the process instead of ending the program with the VM's error line (#778) — neither backend registers unwind information for the code it emits, so an exception thrown in a call the JIT's bridge made (the interpreter it nests for a callee with no native code, a trap, or the bridge's own opcode switch) could not pass through the compiled frame: libc++ terminated
obr(SIGABRT,terminating due to uncaught exception) before Execute's catch printed anything, and libstdc++ and Windows end the same way. Interpreted, the same program stopped with>>> virtual machine: internal error: ... <<<. The bridge's two entries catch the exception now and report it as Execute would: the same line, the stack from the compiled method down, exit status 1.jit_bridge_exception.obsaborts the old VM in both regression passes, and the flag tests' section 8 names the status and the message, which the regression runner (any non-zero exit with output) cannot tell from the abort String->ToFloaton text that is not a number killed the program on its tenth call (#778) — the JIT bridge's S2F ranstd::stodbare, so once the library method was auto-compiled (its tenth call, by default) the first unparseable string threwstd::invalid_argumentinto compiled code and aborted the process, where the interpreter'sStr2Floatgives 0.0: a program parsing text answered 0.0 nine times and died on the tenth. The bridge's S2I caught that but letstd::out_of_rangethrough the same way for an integer that does not fit, where the interpreter gives 0. Both answer as the interpreter does now;vm_jit_equiv.obsparses bad text for twelve rounds across the switch to compiled code, and the old VM aborts on round ten- A method taking a func-ref parameter went wrong once the compiler inlined it (#763) -- the inliner shifts a pasted callee's local slots past the caller's, but it shifted only the
IntandFloatvariable instructions; the func-ref loads and stores kept the callee's ids, so the reference was stored over the caller's own locals and the call went through whatever was there. The interpreter looped forever, the JIT returned garbage; any one-return helper of the shapeApply(x, f) { return f(x); }called from a loop outsideMainhit it. The func-ref instructions shift with the rest now.inline_funcref_param.obsfails on the old compiler in every VM mode, andvm_jit_equiv.obsinlines such a helper into a compiled loop - ARM64 JIT: compiling a method holding a patched func-ref call ended the process —
PatchCallSitesrewrites a func-ref site intoDYN_MTHD_CALL_JITwhen a method whose ids equal its operands compiles; the ARM64 pre-scan accepted that opcode and the instruction switch had no case for it, so the compile reached the default ("Unknown instruction", exit 1). It is handled as the same call, as on AMD64 Runtime->SetLocalewith a name the system lacks ended the program — the VM switched the C library's locale and then built astd::localefrom whateversetlocalereturned, unguarded, in every build but Windows x64 (which only calledsetlocale). For a name the system does not havesetlocalereturns NULL, and on macOS it accepts a per-category composite (C/C.UTF-8/C/C/C/C) that libc++ still cannot build; the constructor threw on either. Interpreted, the program stopped with>>> virtual machine: internal error: locale constructed with null <<<while POSIXobrstill exited 0; with the JIT on, the exception unwound through compiled code and the process aborted. Such a name is now refused rather than replaced with a fallback the program did not ask for: the C library's locale and the console's are left as they were, and thelocaleproperty is not set. A name the system has is accepted as before, except on Windows ARM64, where even a valid name stopped the program: the VM imbuedstd::wcoutwith the named locale, so wide output reached the Unicode-mode stdoutobrsets up as narrow bytes, and the C runtime ended the process with 0xC0000409 and no output. Windows now sets only the C library's locale, as the x64 build always has.vm_set_locale_refused.obsaborts the old ARM64 VM on macOS in both regression passes and caught the Windows ARM64 crash in CI, and the flag tests run it with--jit=off, where the old VM's exit status hid the failureobron Linux and macOS exited 0 after a program died inside the VM —Executereturns -1 once it has printed>>> virtual machine: internal error: ... <<<,>>> virtual machine: out of memory <<<or>>> load error: ... <<<, but the POSIX entry point discarded that result and returned 0, so a program that ended in one of those lines reported success to the shell, aMakefileor a CI step, and only a reader of stderr knew; Windows returned the -1. First seen on macOS whenRuntime->SetProperty("locale", "xx_YY.bogus")killed the program withlocale constructed with null(that call is made to survive separately);"1e999"->ToFloat()reaches the same catch on every platform. The POSIX entry now exits 1 after such an error, the status every other failure ofobralready uses there (a runtime trap, a bad flag); a program that runs to the end still exits 0, andRuntime->Exit(n)still exitsn, since the trap callsexititself. A command line that is all flags (obr --jit=off, no program) exited 0 with nothing printed on POSIX and -1 with nothing printed on Windows; both print the usage and exit 1 now.vm_error_exit.obsfails on the oldobrin the regression suite (# EXPECT_RUNTIME_ERROR), and the flag tests check the message and the status- ARM64 JIT: a func-ref parameter followed by any other local overwrote it — the frame reserved two words for a func-ref but placed its slot at the pair's upper word, and the store wrote the pair upward from there: the reference's second word landed in the next local's slot (a plain function reference's closure word is 0, so that local read as zero), and the collector, walking the pair from the slot, read it one word low. Hidden because every fixture's func-ref parameter was last in its list or inlined away;
jit_entry_shapes.obsfails on the old layout with every method compiled - ARM64 JIT: every compiled method returned zeros in its caller's
D8-D15— the entry zeroing ran from the first spill slot through the locals, across the eight save slots the prologue had just written, so the epilogue restored zeros into the callee-saved float registers. No C++ caller keeps a value there across the call, which is why nothing noticed; the zeroing skips the save slots now, and the save is emitted only when the method takes one of those registers - JIT (both backends): a compiled method with a declared-but-unreferenced local had its frame scanned under the wrong types — the JIT laid out a native slot only for locals that some instruction references, while the collector walks a compiled frame by declaration, one slot each. A local the code declares but never touches shifted every slot below it by one word under the collector's eyes: an object where an object array was declared had its first field taken for a length, and the walk ran off the heap. Found on Linux with every method compiled (
OBJECK_JIT_THRESHOLD=1, which CI does not run there):Web.HTTP.Server.HttpRequestHandler:ServeOnebrought the collector down inmcp_debug_test; a nine-line reproducer crashes the shipped Windows binary the same way. The frame is laid out from the declarations now, one slot per declaration after the and/or temp, on AMD64 and ARM64.jit_frame_unreferenced_local.obsfails on the old layout in both JIT modes - AMD64 JIT: a constant character stored into an array element through
R8-R15released the register twice — the char store'sR8-R15path copied the element address to a second register, released the element holder there, stored through the released register, and released the holder again at the end: two entries in the free pool for one register, two later owners, and a miscompiled method that crashed. Reachable whenever the element address landed in an extended register; the calling-convention work's result pop made that the common case, which is howcore_arrays_simpleandarm64_char_arrayscaught it. Both store encoders take an extended base, so the copy is gone with the double release;vm_jit_equiv.obsstores constant characters and bytes right after call results - A compiled caller of a
virtualmethod ran slower than the interpreter — the JIT's callback bridge looked the callee up by the call site's operands, which name thevirtualdeclaration rather than the override; the declaration has no native code and never gets any, so every such call built a fresh interpreter (a heap allocation and a root registration under the global lock) to resolve and run the override: 125 ns per call against the interpreter's own 70 ns. The bridge now resolves the override through the receiver's class, as the interpreter does, and the call takes the compiled path: 20 ns. That lookup miss was also all that kept the compiled path from running the declaration's empty body; it refuses one explicitly now. An inherited override's resolution is cached on the receiver's class, where the next lookup asks, instead of on the class the walk ended at, where it missed on every call. Probes invm_jit_equiv.obs; design and numbers indocs/JIT_CALLING_CONVENTION_DESIGN.md - Nested string concatenation produced the wrong string, and could crash (#750) — the compiler lowers a concatenation to "allocate a
System.String, store it in a temporary, then for each part evaluate the part, load the temporary andAppend", and that temporary was named after the enclosing method, so every concatenation in a method shared one. A concatenation nested inside another, such as a call argument that is itself a concatenation, overwrote the slot the outer one was still accumulating into:Id(a + b) + "Z"gaveABABZ, and"Z" + Id(a + b)gaveAB, losing the"Z"entirely. One shape segfaulted. Each concatenation now gets its own slot. This was equally wrong in the interpreter and under the JIT, so the interpreter/JIT equivalence fixture could not see it;string_concat_nesting.obschecks expected values and fails on the old compiler - An interpolated string on the left of a concatenation printed its value twice (#752) —
"{$a}" + "{$b}"gaveAAB. The concatenation builder analyzed its leftmost operand once to learn whether the+was a string concatenation at all, then analyzed every operand in its list, the leftmost a second time; a literal's analysis appends its interpolated segments on every visit, so the left value was emitted twice. Right-hand operands were visited once, which is why only the left side was wrong. The leftmost operand is now analyzed once, and a literal's analysis is idempotent. Wrong identically in the interpreter and under the JIT;string_interp_concat.obschecks expected values and fails on the old compiler - JIT loops never reached a GC safepoint (both backends) — the loop-header poll was emitted at the label and keyed on the label's index, but a jump's target is the instruction after the label, so it never matched; a collection triggered by another thread waited for the whole loop. The poll is at the target now, and
jit_gc_safepoint.obsfails on the old code - A compiled loop kept a stale
selfafter parking at its safepoint (both backends, #746) — when a thread parked at a loop-header safepoint while another thread's collection promoted (moved) the young object the loop's method runs on, the collector relocated the frame'sselfbut not the JIT's private copy of it, which only the callback path refreshed. Every field read and write in the loop then went through the old nursery address until the next callback: wrong values, and writes into whatever the recycled nursery now held, other threads' objects included. JIT-only, multi-thread-only, on every platform, present since the poll started firing. The poll's slow path now re-readsselffrom the frame, as the callback path does.core_thread_gc_stress.obsruns compiled again and fails on the old code (35–45 corruptions on every CI leg; 5–34 on a 4-core affinity locally, 0 after) - AMD64 JIT: constant division with the dividend in
RDXmultiplied the magic constant by itself; a call, a loop and anif/elsebefore the division produce that register assignment - ARM64 JIT: a method's slot 0 vanished when its
&/|sat only in conditions — the compiler reserves local slot 0 in such methods and the ARM64 collector skips it, but the JIT laid the frame out from the referenced ids alone, so the collector read every declared slot one off and followed integers as pointers; both backends now reserve the slot whenever the method claims it.OBJECK_GC_TRACE=1names each JIT frame and slot the collector scans - ARM64 JIT: a floating-point register leaked per fused float compare, so a method with more of them than the pool holds silently ran in the interpreter
- AMD64 JIT: char-array elements through
R8–R15— the 16-bit movers emitted no REX prefix, so the element was read from or written to the low register with the same number; dormant until the allocator gainedR8–R11 - AMD64 JIT:
>>was a logical shift — the interpreter shifts a signedINT64(arithmetic) and the ARM64 backend emitsasr, so-8 >> 1was-4in three places and9223372036854775804in the fourth: every negative operand of>>in JIT'd x64 code has been wrong, in every release. All three emission sites (immediate, register and memory shift counts) useSARnow; the equivalence fixture's new shift probe diverges on the shippedobrand matches - AMD64 JIT:
?->on a nil receiver in a JIT-compiled caller exited the process instead of yieldingNil. The backend treatedTRY_START/TRY_END(what?->desugars to) as no-ops, on the recorded assumption that method calls were never JIT-compiled — they have been for some time — so no recovery handler was ever registered in native code. A method containing a try region is now left to the interpreter, as ARM64 always did;nil_safe_opspasses with every method JIT-compiled (OBJECK_JIT_THRESHOLD=1), which is how it was found - Debugger commands that reported success while doing the wrong thing (#717) —
unwatchwith no id removed every second watchpoint (erase-and-advance);set x = "str"stored 0 and reported it;delete <id>deleted the breakpoint at the current line;p 0 % nwas refused andp n / 0divided the debugger process by zero;watchescould not say what it watched (the same empty field reached the DAP stop payload);info class=Chid a class holding only statics; any leading whitespace broke every command;frame Ndereferenced an unguarded class;deleteleaked. String comparisons compared pointers, sotext <> "other"was true for equal text. Sixteen tests, each proven to fail on the pre-fix binary. DAP:evaluateignoredframeIdand answered from the top frame; three more requests lied in the same shape - The language server crashed at startup (#717) — the GC's
IsAllocatedyoung-generation range test accepted an unaligned pointer (…ffffffec) and walked garbage; root-caused with a hand-built Win32 debug loop, 0/200 after the fix (IsYoungCandidate: 8-byte aligned, inside the region) Game.OpenGL: the overlay drew everything upside down — every label, HUD bar and crosshair in every program using anOverlay; the overlay's quad mapped screen-space y=0 to the texture's bottom row after the (correct) upload flip. Fixed in the overlay's own quad. Shipped in v2026.9.0; found from a screenshot, not a test- macOS ships an uninstaller — the
.pkgwrote to four places (/usr/local/objeck-lang,/etc/paths.d/objeck, a LaunchDaemon, anexport OBJECK_LIB_PATHline in/etc/zshenvand/etc/profile) and shipped no way to undo any of them; the shell edit now carries a marker so removal cannot match a line the user wrote - Every hash of a string was wrong —
Hash->SHA256("abc"->ToByteArray())digested"abc"plus a zero byte, so no digest, HMAC or signature computed by Objeck ever matched another implementation. AByte[]has two size words:Size()reads[2], native libraries read[0]. The interpreter'sByte->Newwrites[0] = count, but the JIT'sNEW_BYTE_ARYand twelve trap producers (Char[]->ToBytes, the zlib/gzip/brotli traps, the HTTP/2 and HTTP/3 response bodies, string literals, the serializer's buffer) wrote[0] = count + 1for the NUL pad, and a comment called that the standard layout.[0]is the element count everywhere now; the pad stays in the allocation only.byte_array_header_test.obshashes arrays from each producer and compares with known digests - JIT:
IMULwrote its product to the wrong register —mul_reg_regencoded0F AFwith the ADD family's operand layout, so the result landed in the source register; its one caller compensated by passing the arguments backwards. Found while adding the missing 64-bit guard tomul_imm_reg(add/subhad it since v2026.8.4):n * 4294967311multiplied by 15. Both fixed;vm_jit_equiv.obscompares the interpreter and the JIT on the shapes involved - JIT: a native-call temporary clobbered live values —
call_xfunc/call_xfunc2(the path behindPow,Log,Sin, …) spilled nothing, so7 + x->Pow(5.0) + Bump(x)evaluated differently compiled than interpreted. Live working-stack registers are spilled around the call - JIT: relinking retargeted
selectdefaults —PatchCallSitesmatched on operands alone, so aJMP_TABLEwhose (base, range) happened to equal a (class, method) pair had its default arm rewritten to instruction 1. Only call opcodes are patched - A debugger halt could never land —
haltwas a plainboolwritten by other threads and spun on by the dispatch loop; the-O3 -fltobuild was free to keep it in a register. It isstd::atomic<bool>, as the structurally identical safepoint flag already was - DAP
setVariableon an expanded object's field wrote a top-frame local — the handle decode had no upper bound, so a child handle (100000+) became a frame index thatDbgFrameAtclamped to the top frame: editingobj.countoverwrote a local namedcountand reported success. The CLIsetcommand and the DAP request now share one path (ResolveAssignable→AssignSlot), fields of an expanded object and elements of an expandedInt[]/Float[]are assignable through their handles, the value is an expression evaluated in the stopped frame (count + 8,0x2a) from either surface, and refusals carry one message.dap_setvar_test.obshas a local and a field of the same name so a cross-write is observable EvaluateForDapRawcast every expression to aReference— a hover or watch on an arithmetic expression read a declaration out of an object that had none- Debugger: hex literals never scanned — the scanner's identifier rule claimed any digit followed by a letter, so
0x2awas the identifier "0x2a" andprint 0x2aanswered "unknown variable"; behind it, the number rule's hex state machine cleared itself on the leading0, and the literal was 32-bit fromwcstolthrough the token toIntegerLiteral(longon Windows), so0x7fffffffffevaluated to -1. All fixed;hex_literalruns in both CLI harnesses - A runtime property could be set only once per process —
Runtime->SetPropertystored withstd::map::insert, which leaves an existing key alone, so every set after a property's first was dropped without a word. The library relies on overwriting:String->SetIntFormat,SetFloatFormatandSetFloatPrecisionstore the formatsInt->ToStringandFloat->ToStringread, in the interpreter and under the JIT, so a number format could be changed once and never changed back (hex and then dec printed 255 asfftwice), andRuntime->GetProperty("locale")kept the first name given toRuntime->SetLocale. BothSetPropertyvariants, Windows and POSIX, assign now.vm_set_property_overwrite.obsfails on the old VM in both regression passes and with--jit=off - On Linux and macOS, a first
Runtime->SetPropertykept the runtime's own properties from ever loading — the runtime fills its property map (install_dir,lib_dir,user_dir,tmp_dir, andconfig.propfrom the working directory) the first time the map is used while it is empty.GetPropertydid that everywhere andSetPropertydid it on Windows, but the POSIXSetPropertystored its key into the empty map without it, and a map holding one key is never empty again. A program that set a number format or a locale before reading any property got""fromRuntime->GetTempDir,GetUserDir,GetInstallDirandGetLibraryDirfor the rest of the run. The POSIXSetPropertyfills the map first now, as Windows always did.vm_set_property_first.obsfails on the old macOS VM in both regression passes and with--jit=off - POSIX runtime divergences from Windows —
Pipe->WriteBytereturnedsend()'s-1astrue;Runtime->Command()dropped stderr (Windows appended2>&1);File->Owner()dereferenced a NULLgetpwuid/getgrgidin containers;CommandOutput()leaked one file descriptor per call frommkstemp - JSON serialized strings without escaping — a value holding a quote, backslash or newline produced text that was not JSON; two of the three AI clients did not know to pre-encode.
JsonElement->ToString()escapes string values and object keys; the pre-encodes inollama.obsand the example programs are gone (they would now be escaped twice).json_escape_test.obsround-trips control characters Ollamaswallowed error replies — of ten inlined response parsers, one bound itselse-ifto the wrongif, so an{"error": …}body (the shape Ollama sends) was silently aNilresult and a non-JSON body dereferencedNil; two paths decoded the reply twice. OneParseResponse/ReportErroronEndPointserves all ten,ParseGenerateResponse(body)is testable without a server (ollama_parse_test.obs)--lib-pathdocumented the wrong thing — it is the Objecklibroot:obrloads<dir>/native/libobjk_*and<dir>/cacert.pemfrom it (obcreads the.oblfiles). The usage text said ".obl libraries"; and the flag test proved nothing twice — its fixture loaded no native library, and Windows resolves the..\lib\nativefallback againstobr.exe's own directory. The test now runs a copy ofobrwith nolibbeside it and requires the control to fail
Testing
- TLS verification is tested for what it refuses —
tls_verify_test.obsstarts a loopback HTTPS server with a certificate forDNS:localhostonly and proves, throughTCPSecureSocketandHttpsClient, that a self-signed certificate is refused without a trust anchor and that a trusted certificate which does not name the host (127.0.0.1) is refused;https_persistence_testhad only ever proved acceptance. Also recorded:WebServeris a process-wide singleton (its server lives in statics), so a secondServeSecurein the same process replaces the first rather than running beside it - A compiled call must stay out of the bridge —
jit_call_overhead.obsguards the release's headline number. Nothing in the suite would have noticed if the F7 native entry were lost and every call fell back to the C++ bridge:vm_jit_equiv.obsproves the answer is right, never that it was cheap. The measure is a ratio,(RealCall - NoCall) / NoCall, which cancels both the iteration count and the machine's speed so it cannot flake on unknown CI hardware; best of three rounds. Validated against the pre-F7 baseline (2cf5500722), where the same.obereports 59.9x and exits 1, versus 6.2x on master; an interpreted run reads ~1.1x, so a platform with no JIT passes rather than failing for the wrong reason vm_write_char_buffer.obscovers theWriteBufferfix from inside the program, where stdout correctness cannot be asserted directly: a partial write must return the count requested (4 of 8, not 8) and a non-ASCII write the character count (3, not the 6 UTF-8 bytes the old path counted). Both read identically for plain ASCII written in full, which is why it uses a partial write and characters above U+007F
Compiler
- A long addition chain no longer takes exponential time to compile —
AnalyzeCalculationrecursed into each calculated operand and then analyzed it again throughAnalyzeExpression, so every level of a nested expression was analyzed twice: twenty terms ofa + b + ...compiled in three seconds, twenty-four in over a minute, and a thirty-term sum never returned (nine gigabytes and counting). Each operand is analyzed once now; a 37-term chain compiles in 0.05 s, with bytecode byte-identical to the old compiler's on the JIT fixture and the concatenation tests.compiler_long_add_chain.obssums forty terms - Conditions branch directly —
&,|and<>in anif,while,do/while,foror?:condition become short-circuit jumps instead of materializing a value through a temporary local (a store, a reload and acmovper connective in JIT code); evaluation order is unchanged and now pinned bycore_bool_short_circuit.obs - A
nativemethod is no longer inlined into a caller that is not itself native — the-opt s3inliner pasted small calls into their caller and the copy ran in whatever engine the caller did;Mainwas exempt, but a thread'sRun(called once per thread, so never auto-compiled) was not, so a worker thread's native loop ran in the interpreter, ~50x slower than the same code on the main thread. Pinned byjit_native_inline.obs - Name resolution stopped copying the library maps —
Linker::GetAll{Aliases,Classes,Enums}Mapreturned the wholeunordered_mapby value andSearch{Alias,Class,Enum}Librariescopied it to perform one lookup, from 75 call sites; a 4,087-line application spent 20% of its compile hashing wide strings for those copies.Class::AddMethod's duplicate check walked every method and copied every parsed name — anunordered_setnow, O(1), insertion order preserved CheckIdentifieris a map lookup — a 366-line switch restated every keyword the map already held (a token listed in only one silently scanned as an identifier —true,false,Nilandthreadswere each dead on arrival in the debugger's copy for that reason), andoperator[]inserted every non-keyword identifier in the source into the keyword table. Both scanners consult anunordered_mapand never grow it- Linker copies —
SearchClassLibraries(name)copied the whole class map per call in the innermost resolution loop;GetInstructions(),GetMethods()andGetClasses()returned their containers by value. All by reference now
Infrastructure
- Release shore-up — the notes above now cover everything merged since the tag (they had omitted #716–#719 and the OpenGL work), and the README shipped inside the archive lists it; the LSP hover/completion index is regenerated from the current
lib_src(it was five days and one library API behind); the VS Code extension manifest is stamped byupdate_version.ps1instead of sitting two releases stale;verify-signing-credentials.ymlnow checks the credentials the pipeline actually uses -- the two Apple identities, imported with the release's own commands -- and runs weekly, so a rotated password or an expiring certificate shows as a red schedule instead of an unsigned release (it had been checking the retired WindowsCODESIGN_*secrets, whose stored certificate turned out to be the Sectigo intermediate CA with no private key; Windows signs locally with the eToken); the 31-line library-build list thatupdate_version.shand CI's Windows step each carried is onebuild_libs.sh;code_doc64_debug.cmdlistssdl_gl;code_doc64resolvesobc/obrfrom its own directory (under Git Bash,NoDefaultCurrentDirectoryInExePathmade the bare names fail) and refuses to package anapi.zipthat holds no pages -- it had produced a three-file archive and reported success;docs/third_party_dependencies.mdrecords where every platform's TLS/HTTP/media libraries come from, anddocs/release_integrity.mddesigns the signed manifest that would letobudetect a substitutedSHA256SUMS - The standard libraries are rebuilt by the compiler that ships with them (#792) — the committed
.oblset dated from 2026-09-04, and four compiler correctness fixes landed after it: the func-ref inliner (#763), both string-concatenation accumulator fixes (#750, #752), and the native-inlining rule.build_libs.shcompiles every library with-opt s3, which is exactly the inliner #763 fixed, so the libraries were built by a compiler that still pasted a func-ref parameter's slots unshifted. CI does not cover this: only thebuild-docsjob runsupdate_version.sh, while thebuildjob that produces the shipped artifacts uses the committed set, and the regression suite runs against those same libraries so it could not have caught it either. 30 of the 32 libraries changed on rebuild, and they got smaller (xml.obl8779 → 8137,sdl_gl.obl51827 → 50203) - A Windows deploy stops dirtying eight tracked files (#787) —
update_version.ps1stamped throughSet-Content, which joins lines with CRLF, against a repository declaredeol=lf; everydeploy_windows.cmdrun leftversion.h, the five.rcfiles,readme.jsonand the VS Codepackage.jsonmodified with an empty content diff, and the nextgit pullrefused to merge - The weekly secret scan failed every Monday (#718) —
gitleaks-actionscans the pushed range on push/PR but the whole history on a schedule, so the weekly run reported 326 findings from 2014-era test data and was never green; a job that is always red is a job nobody reads. Scheduled runs use the same baseline the push scan reasons about, verified by a positive control that omits an already-public 2014 commit from the baseline and fails - POSIX deploys verify their native libraries —
verify_native_libs.sh, the counterpart of the Windowsverify_native_libs.ps1, runs after the native copies indeploy_posix.sh,deploy_macos_arm64.shand both MSYS2 deploys: a library whose build failed was dropped from the release with a green exit (the obu incident, again). The MSYS2 deploys also invokedbuild_msys2-*.shscripts for lame and opencv that did not exist update_version.sh [amd64|arm64]—update_version_arm.shwas a copy with four characters changed and is a shim now; both stop on the first failed compile (set -e) instead of shipping a stale.oblgreencore/vm/arch/jit/README.mdstated three things the code does not do (method inlining,JMP_TABLEcompilation, ARM64 loop detection as a blacklist)- A manual declaration nothing reads is not a declaration (#692) — the
docstoken inRELEASE_MANUAL_STEPSwas inert:check_release_config.shreported the configuration OK, then the v2026.9.0 publish failed in "Deploy API Documentation" anyway, because that job had a hard-fail on the missing secret and no branch reading the variable beside it. Sourceforge and playground each had both halves; docs had only the second. The gate now greps the workflows for a branch reading each token and reportsINERTwhen there is none - Signing is not finished until the manifest matches (#693) — signing rewrites the MSIs, so the
SHA256SUMSpublished beside them describes bytes that no longer exist andsha256sum -cfails on both Windows installers.sign_release.cmdre-uploaded the signed MSIs and never touched the manifest; the repair lived inpost_release.sh's gate 4 and in whoever remembered. Newtools/cicd/update_sha256sums.ps1rebuilds it from the published assets, writes LF, and re-downloads its own upload to verify - The playground deploy that ships a fix now runs the fix (#694) —
update.shpulls the repo from inside itself, so a run that pulls a newupdate.shkept executing the old one and exited 0. The first v2026.9.0 deploy printed "Update complete", moved/api/healthto the new version, and left the engine on 2026.8.4. It now re-execs when the pull changes it. The health check also polls for 20s instead of sleeping 3s and asking once, which had it reporting failure after every healthy restart - An open issue must be fixed or explained before a tag (#695) — releases were cut without consulting the issue list, so an issue could sit open across several releases having never been considered, indistinguishable from one deliberately deferred.
tools/cicd/check_open_issues.shrequires each open issue to be fixed or carry aDeferred from v<VERSION>:note, which it prints; a note naming an older version does not carry forward
Virtual Machine
- A call between compiled methods costs 16 ns instead of 26 (
docs/JIT_CALLING_CONVENTION_DESIGN.md, phase 1) — the bridge every compiled call crosses took two critical sections and cleared a 768-byte frame buffer per call. Frames come from a per-thread pool with no lock now, zeroed on acquire for exactly the local space the method declares (which is what a local that is declared but never assigned relies on to read as 0 or Nil), and a call whose callee is bound at compile time enters the bridge with the method itself rather than an opcode to switch on and look up. Recursion (Fib(32)): 0.22 s to 0.13 s compiled, 0.39 s to 0.25 s interpreted, since the interpreter's calls cross the same pool; a compiled loop calling a field-touching method 0.53 s to 0.34 s. The register-argument convention that removes the rest is phases 2 and 3 of the design - A compiled method's entry and exit are a third shorter (AMD64; the design's phase 2) — a method that never uses a float register no longer saves and restores
XMM10-XMM15on Windows (the blocks are patched into a jump over themselves once the body is emitted), the frame is zeroed with straight stores instead of a microcodedLOOP, and the operand-stack arithmetic around parameters, returns and call results is computed once per sequence rather than per value. A compiled call goes from 16.3 ns to 14.4 ns; what remains is the C++ bridge, which the design's phase 3 replaces - A compiled method's entry and exit are shorter on ARM64 too (the design's phase 2, its section 12) — the arguments are read below one
toppointer and the results popped with one scaled load, the frame is zeroed withstp xzr, xzrpairs instead of a five-instruction loop (nineteen trips for a one-local method),D8-D15are saved and restored only by a method that takes one, and the prologue's frame-size immediate is computed exactly rather than ORed onto a template that held 96. A compiled call goes from 17.1 ns to 13.4 ns on an M4 Max,Fib(32)from 0.135 s to 0.103 s;jit_entry_shapes.obscovers the shapes the entry and exit take - The ARM64 JIT allocates from twelve general registers (F4 on ARM64) —
X12-X15joinX0-X7, handed out after them, so a method whose expressions carry a ninth to twelfth temporary compiles instead of falling back whole; the backend does not spill, and the bytecode pushes every term of a chain before its first operation, so a ten-term sum needed ten registers.jit_entry_shapes.obs's eleven-term sums compile now and fell back before (a ten-term version fit Windows x64's ten registers exactly and exposed a miscompile there, #773); the six loop kernels ofjit_probe.obsand the call probe are unchanged within noise - A compiled call on ARM64 goes straight into the callee (the calling-convention design's phase 3 and native entry, its section 13) — every compiled method gains a second prologue a compiled caller calls with self, its stack pointer and the end of its outgoing area in
X0-X2, the frame record built in the callee's own frame and pushed on the call stack;RTRNreturns the value inD0;virtualand func-ref sites keep inline caches. The C++ bridge is the slow path. A compiled call goes from 13.8 ns to 7.4 ns on an M4 Max,Fib(32)from 0.107 s to 0.050 s, avirtualcall loop from 0.0345 s to 0.016 s - ARM64 JIT: a float pending across a libc call no longer sends the method to the interpreter — the libc helpers behind
Sin,Cos,Pow,Logand the rest spilled pending integer temporaries around the call but gave up on a pending caller-saved float; each is parked in a free callee-savedD8-D15register for the call now, twofmovs and no memory.Arith:Mix, the equivalence fixture's one standing fallback on ARM64, compiles, andOBJECK_JIT_REPORT=1on the fixture is silent there for the first time - A compiled method calls a compiled method directly (AMD64; the design's phase 3, first step) — a call between compiled methods went through a C++ bridge: an eleven-argument entry, a frame from the pool, a release. The caller now reads the callee's entry address from one word on the method, builds the callee's frame record on its own stack, pushes it on the call stack and calls the entry itself; a callee without native code yet, or a full call stack, takes the bridge as before, and a callee's error status is reported by
JitNativeCallErrornaming both methods. A compiled call goes from 14.4 ns to 7 ns andFib(32)from 0.115 s to 0.071 s; on master the call was 26.5 ns andFib(32)0.208 s.virtualand func-ref calls still take the bridge. Two tests cover the exits: a directly called callee that dereferences Nil, and recursion past the call stack's limit - A
virtualcall from compiled code costs what a bound one does (AMD64; the design's phase 3, second step) — eachvirtualcall site keeps a small inline cache of receiver class, resolved target and entry; a hit compares the receiver's class word with the last record and calls the entry directly, a miss resolves the override through the receiver's class and fills a record, and a site that sees more classes than it holds stays on the bridge. A monomorphic virtual call goes from 18 ns to 7.5 ns.vm_jit_equiv.obsexercises a six-class site beside a monomorphic one - A func-ref call from compiled code takes the same cache (AMD64; the design's phase 3, third step) — a call through a function reference or closure is keyed by the func-ref's packed word, so it too calls its target's entry directly after the first call; about 15 ns per call, the bridge's share, are gone. The patched
DYN_MTHD_CALL_JITopcode, which the interpreter writes at a site when a callee with matching operands compiles, is now emitted as the same call; a method holding one failed to compile before.vm_jit_equiv.obsexercises one site alternating between two references beside one that keeps a single reference - Compiled methods take their arguments without the operand stack (AMD64; the design's section 11, the register-argument entry) -- every compiled method now has a second entry that a compiled caller enters with
selfand the caller's frame pointer in registers and the arguments in the caller's outgoing area, its frame record built in its own frame; the result comes back inXMM0instead of through the operand stack, and the caller's marshalling, the callee's count traffic and the sections-8-to-10 record building on the caller's stack are gone. The bridge entry stays for the interpreter and for a callee not compiled yet. A bound call goes from 7.0 ns to 5.5 ns and a virtual one from 8.0 ns to 6.0 ns; where the cost is latency rather than instruction count the gain is larger:Fib(32)from 0.072 s to 0.043 s (0.208 s on master before the design), and a func-ref call to less than half.vm_jit_equiv.obsgains probes for every value shape across such a call (Floatarguments and results as literals, locals and registers; a call with no arguments; eight arguments; aNilresult; receivers held only by the callee while it allocates; a func-ref result, which keeps the bridge), byte-identical across the three modes - The Linux x64 JIT allocates the same eight registers as Windows -- the POSIX backend had four pool registers and three auxiliary ones where Windows had
R8-R11in the pool since batch 3, so an expression with five live values, or a method with a dozen locals, quietly ran in the interpreter on Linux (OBJECK_JIT_REPORT=1on the equivalence fixture named two of its own probes).R8-R11join the Linux pool, the prologue no longer pushes them (they are caller-saved, and every path that calls out spills or pushes them), and the write barrier saves the four of them.RealCall0.140 s to 0.128 s on Linux - AMD64 JIT: a quarter less machine code for the same programs -- every memory operand was encoded with a four-byte displacement (the
ModRMhelper fixed mod=10), so a frame slot or a field took seven bytes where four do; every add, subtract and compare of an immediate against memory took a four-byte immediate; a boolean result took four instructions and a second register (mov 0,mov 1, compare,cmov); the prologue zeroed the frame one word per eleven-byte store. Displacements and immediates that fit in a byte now take one (mod=01,0x83), a boolean issetccandmovzx, and the frame is zeroed sixteen bytes at a time from a clearedXMM0. The equivalence fixture's 84 methods went from 292,958 to 222,755 bytes of code, byte-identical output in every mode; the call kernels gained a few percent - The hottest integer locals of a loop live in registers (AMD64: up to three in
R13–R15) — loaded at the loop header, read and written as registers inside, stored back on every exit;OBJECK_JIT_REPORT=1names each pinned loop andOBJECK_JIT_PIN_MAX/OBJECK_JIT_PIN_SKIPturn it off for bisecting. Design indocs/JIT_LOOP_LOCALS_DESIGN.md
The measured findings of docs/JIT_CODEGEN_ASSESSMENT_2026_09.md (#728), landed in #731, #732 and #733 with the interpreter/JIT equivalence fixture, the flag tests and the regression suite (normal, and with every method JIT-compiled) green, and all five CI legs green on the combined tree:
a->Size()is inlined by the JIT (AMD64 and ARM64) —LOAD_ARY_SIZEwent through the interpreter callback: push the array onto the VM operand stack, call C++, read the result back, ~35 instructions and a call per evaluation ofi < a->Size(). It is a nil check and one load now. An array-summing loop went from 6.0 to 0.75 ns per element (8×); a float dot product 2.7×- Division by a constant is a multiply (AMD64 and ARM64) — every constant divisor other than 0 and ±1 (and, on AMD64, powers of two, which already used shifts) now goes through its Granlund–Montgomery magic number: a 3-cycle
imul/smulhand a few shifts in place of a 10–20-cycleidiv/sdiv. The recipe was checked against exact division for 480,000 cases includingINT64_MIN,INT64_MAX, negative divisors and powers of two before it was emitted, and the equivalence fixture holds divisors of every shape. The assessment's integer loop (two constant divisions per iteration) runs 2.5× faster - Constant divisors skip the divide-by-zero check, and AMD64 saves
RAX/RDXaround a division only when they hold something — they were saved and restored unconditionally - The AMD64 allocator has eight general registers —
R8–R11joinRAX–RDX(on Linux and macOS they were already reachable through the auxiliary pool). Four registers meant any expression with five live values spilled, or the method fell back to the interpreter: the equivalence fixture's own twelve-term division probe did exactly that, silently. Ten spill slots instead of six in the frame, and the write barrier's slow path saves the whole pool - A dense
selectis compiled as a jump table (AMD64 and ARM64) —JMP_TABLEwas in neither backend's whitelist, so any method holding aselectran in the interpreter whole: parsers, tokenizers, state machines and dispatch loops, the code written withselect. It is now a bounds check and an indirect jump through a table of 32-bit offsets placed inline after the jump (no data section, PC-relative). A tokenizer-shaped state machine over a 1,160-character buffer, 2,000 rounds: 0.099 s interpreted, 0.002 s compiled. The equivalence fixture gains eightselectshapes (dense, gaps to the default, a negative base,Charlabels, noother, nested, abreakout of a pinned loop, a method-call selector). Design:docs/JIT_SELECT_TABLES_DESIGN.md Main, a thread'sRun, and any once-called method with a loop are now compiled — the auto-JIT counted calls, and these are called once, so a hot loop written at top level ran in the interpreter for the life of the program (~50x slower than the same loop in a helper; the documented workaround was anativehelper). A method with a loop is compiled on its first entry or call; straight-line methods keep the ten-call threshold; a raisedOBJECK_JIT_THRESHOLD(as the LSP server sets) turns both rules off. Found on the way: the loader's classless$Initialization$routine's call toMainwas never patched to the JIT opcode, so a compiledMainwas still interpreted for its one call — dispatch now follows the callee's native code. A 2e8-iteration loop: 3.65 s inMainand 3.52 s in aRunbefore, 0.08 s and 0.13 s after (jit_entry_compiled.obs). Design:docs/JIT_ENTRY_COMPILE_DESIGN.md- The hottest
Floatlocals of a loop live inXMM6–XMM9on Windows — the float half of loop-local pinning: up to fourFloatlocals per loop stay in callee-saved XMM registers for the loop's extent, written back on every exit, alongside the three integer pins inR13–R15. Linux and macOS keep every XMM caller-saved, so nothing changes there (ARM64'sD8–D15are the follow-up). The assessment'sFloatDotkernel: 0.048 s → 0.021 s. Six new fixture probes cover accumulators, a float through a nested loop, mixed int/float pins, more floats than registers, abreak, and calls inside the loop - Array element addressing is one instruction (AMD64
leawith a scaled index; ARM64addwith a shifted register) — the index and the bound were both shifted by the element size before the compare, which changed nothing about the comparison, and the address was then built with two adds - Windows: JIT code saves
XMM10–XMM15— the allocator hands out those registers and the Windows x64 ABI makes them callee-saved; the JIT'd function is entered by a plain C++ call whose MSVC-compiled caller may keep values in them across the call, and nothing saved them. 96 bytes in the prologue, restored in the teardown; Linux and macOS have no callee-saved vector registers OBJECK_JIT_REPORT=1names every method the JIT hands back to the interpreter and why (an unsupported opcode, or a compile that failed part-way, with the instruction index). One unsupported instruction returns the whole method to the interpreter and nothing said so — the equivalence fixture's own division probe turned out to be interpreted because a twelve-term expression exhausted the AMD64 allocator's four registers- The interpreter/JIT equivalence fixture gains probes for
Size()in a loop condition and body, a multi-dimensionalSize(), and constant divisors of every shape (small, negative, above 32 bits, composite) --jit=off/--jit=<calls>and--lib-path=<dir>— the JIT tunables existed only as environment variables documented in a comment insidejit_common.h, so "does it still happen with the JIT off?" could not be asked without reading source. The environment forms remain as the legacy path. Both platform mains carried their own copy of the option parsing and usage text, and they had drifted: agsuffix on--gc-thresholdmultiplied by 2⁴⁰ (a terabyte, not a gigabyte);--objeck-stdioaccepted any value while acting only onbinary, so--objeck-stdio=1silently did nothing; POSIX did not parse the flag and passed it through to the program as an argument; and the Windows flag path discardedSetStdIo's result, sobinaryswitched the console and then toldExecuteit had not. One parser and one usage string now serve every platform, every flag states its valid values, and a value outside them is refused with the expectation named.run_vm_flag_tests.pygates it in CI, including the first automated check that the interpreter and the JIT print byte-identical output
Documentation
- Sourceforge mirrors the GitHub release through a webhook and is not a manual upload; objeck.org has no versioned
/api/v<VERSION>/directories, solatestis the only served path. Both were documented the other way round, in the pre-flight gate's to-do text and in the publish workflow's failure message - The shipped examples folder is curated and indexed (#751 and the follow-up) —
welcome.obsis an executable index that needs no libraries or display and prints, for every example, what it shows and the exact-liblist that compiles it (all verified by compiling them); a scratch hello-world, a JIT bug reproduction and a 4,092-line duplicate of the 2D game no longer ship;tools/cicd/check_examples_index.pyfails CI if an example ships unindexed or the index names a missing file. Two leftovers fixed after: the OpenGL shooting gallery moves to theopenglfolder with the other OpenGL examples asgl_fps.obs, and the second file numbered 19 is nowloops_27.obs
Known Issues
- #722 — the ARM64 JIT miscompiles
String->Equalsin a virtual request-handler callback; the two regression tests that would show it run withJIT_DISABLE, so shipped behaviour is unchanged from v2026.9.0. Scheduled with theSystem.Terminalrelease - #723 —
SHA256SUMSis unsigned, soobuverifies hashes but cannot detect a substituted manifest; design indocs/release_integrity.md, awaiting a maintainer-generated key - #729 — the API-doc parser rejects two
GLWindowmembers (Own,GetDrawableGeneration), so they are absent fromapi.zip, as they have been in every release since they landed
[v2026.9.0] - 2026-09-03
A socket close that stops losing the response on Windows loopback, a language server that no longer serializes every request behind one lock, and a release pipeline that reports what it actually did.
Bug Fixes
- A server that wrote a response and closed could lose all of it — on Windows loopback the reader got a connection reset and zero bytes, even though every byte had been accepted and delivered.
TCPSocketandTCPSecureSocketgainCloseGracefully(), which reads until the peer hangs up and then closes, so the client owns the teardown. Measured over 180 transfers of a 16KB response:Close()lost 21,CloseGracefully()lost none - The language server serialized every request behind one lock — concurrent analysis was correct only because of it, with
TreeFactoryandTypeFactoryas process-wide singletons underneath. They are now bound per thread through a scope guard, so each analysis gets its own and the coarse lock gives way to per-program locking
Infrastructure
- Four publish steps reported success while doing nothing — Sourceforge, the Marketplace, the playground and the API docs each skipped on an absent credential and passed, so v2026.8.4 published with all four green while the playground served a three-month-old engine. A missing credential now fails and names the secret, or is declared manual in one place that also prints as a to-do, and a pre-flight gate checks the pipeline can do what it advertises before the tag is pushed
- Documentation:
docs/windows_loopback_sockets.mdlisted bounded drains at 100ms and 400ms as having no effect, then recommended a drain with a 1000ms bound. Both are true — the bound is the variable, not the technique — but the document did not say so and read as contradicting itself
[v2026.8.4] - 2026-08-30
The release is dominated by one thing: Game.OpenGL, a 3D graphics framework built from nothing to a shipping library over roughly forty commits. Beside it sit a library that had never been usable, a class of JIT bugs that produced wrong arithmetic with no diagnostic, three ways the collector could destroy a live object, and a Windows ARM64 distribution that shipped without the runtimes it needs.
New Features
Game.OpenGL: OpenGL 3.3 core over SDL2, on Windows, Linux and macOS — 26 classes.GLWindow(windowing, frame pacing, key/mouse/wheel input, held-key state, gamepad), built-in shaders with uniforms that no longer leak between programs,Meshprimitives and OBJ loading, textures with anisotropic filtering,Transform/Vector3/cameras (first-person,LookAtPoint, orthographic), materials, up to eight directional/point/spot lights with Blinn-Phong specular, shadow maps including omnidirectional cube shadows from point lights, render-to-texture, instanced drawing plus the object-layerPropBatch, frustum culling on by default,Scene->Raycastfor hitscan and picking, transparency with correct ordering, multisampling, stencil/points/line-width/depth-func, particles, billboards, a transform hierarchy with keyframed animation, sound, and a pixel-space text overlay with a default font. The draw loop stops allocating: per-frame allocations in both original loops went to zero, and the minimal window demo went from 105 lines to 25. Verified by 453 checks that read pixels back rather than merely exiting cleanly- Two 3D demos ship in the distribution:
3d_gl_24.obsandfps_gl.obs, a first-person shooting gallery with hitscan shooting, sliding collision and controller support.run_glmakes setup one command on Linux and macOS Web.Serverreimplemented in pure Objeck: the bundle shipped in every release and could not be used by anyone. Its 13 native entry points appeared in exactly one file — the binding itself — with no.cpp, no build target and nolibobjk_web_serverin any deploy tree; nothing setOBJECK_LIB_WEB_SERVER, andRequestandResponsedeclared no constructor, so an instance could not be obtained at all. The missing native library was never a possible fix:DllProxytakes the library name from a runtime property and the design is a per-host bridge for Nginx, IIS and Apache, whose request structures differ entirely, so a single generic library cannot exist. It is now an adapter overWeb.HTTP.Server, which already provided a complete, testedRequest/Response— same bundle, same class names, same method signatures, no native library, no new build target, no new CI leg. Coverage 0 of 13 methods to 13 of 13- HTTP keep-alive: removes the Windows loopback exposure rather than hiding it, by changing which side closes rather than any socket call. No connection reuse is required for the fix
obu updateandobu rollbackon Windows: the in-place swap needed no copy-self-and-re-exec dance — Windows forbids deleting a running image but permits renaming it.obu checkalso gained offline test coverage on every platform; it had none anywhere, because it built its own request URL instead of going through the function that honors the offline switchTTF_CloseFontis bound, so a font can be released at all- macOS ships as a notarizable
.zip, and the playground deploys from the release pipeline
Bug Fixes
- JIT truncated 64-bit immediates to 32 bits: on AMD64 for
and,or,xor,addandsub; on Windows ARM64 for every one of them, at the call sites rather than the emitters, becauselongis 32 bits under LLP64. Neither crashed nor diagnosed — arithmetic above 2^31 was simply wrong. Windows ARM64 had shipped untested since 2026-02-10, which is why its variant survived - JIT stored float compare clobbered a callee-saved register on AMD64
- JIT
Int[]copy dropped the write barrier - A trap-returned array held elements a minor GC could destroy: arrays are old-generation from creation and objects bump-allocate in the nursery, so a minor collection promoted and moved the elements while repairing references only in objects the write barrier had dirtied — which a freshly filled trap array never was. Measured at 395 of 395 entries destroyed on a single collection, with the reading thread allocating nothing itself
- A value returned by a native library could be collected out of a reused argument buffer
- One failed name lookup shut down Winsock for the whole process:
WSAStartupruns once, so theWSACleanuponIPSocket::Open'sgetaddrinfofailure path dropped the refcount to zero and closed every open socket on every thread. Dormant only becausegetaddrinfoon a literal address does not fail - A short
send()dropped the remainder of the buffer on both platforms - A real HTTP 500 lost its body, and a dead socket reported one; discarded queued response data was not closed, closed sockets read as open, and persistence framing disagreed between HTTP and HTTPS
- A program that started a server and returned normally from
Mainsegfaulted during teardown (Windows): a thread blocked inaccept()never observesHalt, so the drain timed out and~Loaderfreed the program image while that thread was still live;WSACleanupon the way out ofwin_mainthen unblocked every parked socket call process-wide and sent it executing against freed memory. All buffered output was lost with it, so a program that ran to completion looked as though it had done nothing. Linux was always clean for exactly one reason — it has no equivalent call. The image is no longer freed while a thread may still run against it, andWaitForThreadsToDrainnow measures real elapsed time instead of countingsleep_for(1ms)iterations as 1ms each, which on Windows' ~15.6ms timer granularity turned a nominal 2000ms budget into 20–30 seconds of hang on every exit with a live thread - The debugger had the same defect, and there it was not Windows-only:
obdhosts the debuggee's VM in-process, andClearProgramran after every single run — freeing the bytecode image, theStackProgram, and the whole GC heap, sinceMemoryManager::Clearreleases the young region, every old-generation object and the locks the VM takes on the way in. It called neitherHaltAllExceptnorWaitForThreadsToDrain; the strings did not appear anywhere undercore/debugger. Becauseobdoutlives the run and returns to its prompt, an ordinary client connecting to the port the parked thread sits on wakes it with noWSACleanupinvolved — 6 access violations in 6 runs on Windows and 2 in 2 on Linux, against 8 in 8 for the DAP session-exit shape, which was Windows-only. The drain would also have lied had it been called: it reports "drained" when at most one interpreter is registered, andobd, unlikeobr, never registered its own obdcould not debug any multithreaded program: compile one with-debug— which is how a program you intend to debug is compiled — and it segfaulted on the first instruction the spawned thread executed, before that thread produced a line of output.StackInterpreter::debuggerwas set by exactly one constructor, and spawned VM threads use the default one, which left the member indeterminate whileExecute's hot loop called through it on every instruction. It read as a-debugproblem rather than a threading one because the hook touches nothing until a line number exists, and without symbols every line number is -1._DEBUGGERis defined only forobd, soobrwas never affected and the VM regression suite could not have caught it. Breakpoints and stepping still do not reach inside spawned threads, but they run- A failed library load on Windows said nothing about why
- Windows ARM64 shipped without its runtimes: no C++ redistributable, because
VCToolsRedistDiris empty on the ARM64 runner, and OpenCV without its image codecs. Nothing checked either. Also fixed: the 7-Zip probe stored a quoted path and then re-quoted it, and the vcpkg root probe missed~/vcpkgwhile vcpkg's zlib shadowed the vendored one - mbedTLS and nghttp2 came from stale committed blobs, not vcpkg
- SDL2 failed to load on Windows without a hand-set
PATH: the DLLs shipped inlib/sdl, but Windows resolves a dynamically-loaded library's imports against the executable's directory, never the directory holding the library. Every SDL program was affected, and the regression runner hid it by prepending the directory before running the tests. macOS now bundles SDL2 in the distribution instead of asking users to install it - ONNX aborted the process when destroying its
Envat exit on macOS - macOS notarization had been failing on every release, silently
- The doc parser was reading prose as code: the word "bundle" in a comment or string literal re-filed every class after it under a bundle named for the next word. Five files hard-code the library list and had drifted apart — one short two libraries while still naming a deleted third, so the counts matched and nothing looked wrong; a CI check now set-diffs all five against the sources
- Game.OpenGL defects found by review rather than by use: props parented into a hierarchy were culled and sorted in local rather than world space, the instanced shader never wrote
world_position, textures rendered upside down, the projection went stale on resize, an accidentally-public API surface was narrowed, andrun_glnever passedgl_modelits model file
Performance
- Native call names are no longer allocated on every call: a string literal allocates on each evaluation, and the literal naming the native function was 92% of a call's cost — 1655ns to 130ns. Guarded by
tools/cicd/hoist_native_names.py --checkso it cannot drift back - Resolved native entry points are cached, on every platform: every
EXT_LIB_FUNC_CALLconverted the function's name from wide to narrow and then asked the loader for its address —GetProcAddresswalking a DLL's export table,dlsymwalking the module's symbol hash — on every call, for a name that never changes. Measured at 110ns per call, 69 in the lookup and 39 in the conversion feeding it, or about 5.5% of a native call. The cache is thread-local rather than shared behind a lock, since an uncontended mutex would hand back a useful fraction of what it saves; invalidation is a generation counter, so a library unloaded and another loaded at the same address cannot be called through a stale pointer - Game.OpenGL holds constant strings instead of rebuilding them per call, and reuses native-call argument buffers on the per-object paths
Testing
- 125 of 168 assertion-bearing tests printed FAIL and exited 0, so the runner scored them as passes; the suite scores by exit code, and two tests were failing silently inside a green run
- The ONNX and LAME bindings had never executed on any platform; both now have runtime tests
- A regression test for the VM teardown crash that fails against the unfixed VM rather than merely passing against the fixed one, and a standalone reproduction for the Windows send-then-close data loss
- Debugger teardown coverage in both shapes, CLI and DAP, reusing the VM reproducer rather than duplicating it. It reports 2–4 failures on 3 runs out of 3 against a build with the fix reverted, and deliberately never asks
obdto read a second command: the command loop repeats the previous one on an empty read, so areadline()that returns rather than blocks on a pipe would re-run the program forever instead of quitting
Infrastructure
- The Windows ARM64 CI leg could not run a single thing it built;
dumpbinis now located outside the ARM64PATH, and the runner is asked what the OpenCV DLLs actually import - A crashing macOS regression test reached the log as one frameless line; the real backtrace is now collected into the failure artifact
- Release monitoring and post-publish are two tested scripts rather than prose, and the release skill no longer contradicts itself on signing — the certificate is on a hardware token, so CI signing is impossible rather than merely unconfigured
release-drafterstopped advertising a macOS Intel build that has never existed- The native-interface guide corrected two claims that were not true, and now states the generational rule library authors actually need: everything a native library allocates is old-generation, and why
[v2026.8.3] - 2026-08-19
A full static-analysis pass. Coverity Scan now runs on Windows as well as Linux, and this release works through both reports: crashes reachable from library input, two hangs, a class of stream-format leaks, and the unguarded entry points behind every “terminated with no message”. Alongside it, the Windows installer signing pipeline was corrected — it had been reporting signatures it never produced.
Bug Fixes
- Compiler crash on library input:
TypeParser::ParseTypeandParseParametersswitch on the first character of a type string with no default case, so anything outsidel/b/i/f/c/n/m/olefttypenull and fell straight intoSetGenerics/SetDimension. An empty string reaches this too —operator[](0)on an emptywstringyields a null character. Both are called fromlinker.cpp/linker.hon type strings read out of.oblfiles, so a corrupt or crafted library crashed the compiler.ParseParametersalready guarded theSetDimensiondereference, which is how theSetGenericsone above it was missed.ParseGenericsis still called whentypeis null so the index advances exactly as before — only the assignment is skipped - REPL hang on an unmatched brace:
Document::ListandDocument::Saveboth didident_count--on asize_twhen a line starts with}. With the count already0that wraps toSIZE_MAXand the indent loop below runs about 1.8e19 times. A buffer whose first non-empty line is}was enough to hang the REPL, on listing and on save - Lambda silently dropped on a signature collision:
AddMethodreturns false on a collision andBuildLambdaFunctionignored the result, so a colliding lambda was never registered while the code continued toEncodeSignatureandAssociateMethodon it — with no diagnostic at all.ParseAnonymousClassnow checks the same return for consistency, so a future change to itsHasDefaultNew()guard cannot lose the method silently - Indeterminate
func_ref_unwrap: four ofMethodCall's five constructors null it; theNewconstructor did not.IntermediateEmittertests it withif(func_ref_unwrap)and emits through it, so non-null garbage meant emitting a bogus call - Debug session lost to one malformed request: only
json::parsewas guarded, so a message that parsed but carried unexpected types reached the handlers, threw from inside nlohmann, unwound out ofRun()to amain()with no handler, and terminated the process. Dispatch is now wrapped: parse errors are dropped as before (there is noseqto answer), other JSON errors get a DAP error response, andmain()keeps a backstop for the clean exit code - DAP flags read without a lock: seven flags written under
mtxby the DAP thread were read unlocked by the VM thread at every instruction. Beyond tearing, nothing stopped this-O3 -fltobuild hoisting those loads out of the dispatch loop, so a disconnect or step request could go unseen indefinitely. All seven arestd::atomic<bool>now — includingbreak_on_exception, which Coverity never flagged because its writes never holdmtxat all, leaving the least protected flag the one that went unreported - Undecodable console line re-ran the previous command:
ReadLine()calledBytesToUnicodeunchecked, and it appends rather than assigns while leaving prior contents in place on failure — so an undecodable line silently handed back and re-executed the last command. It now clears up front and drops undecodable input - Connect that never completed reported success:
getsockopt(SO_ERROR)was unchecked on both the POSIX and Windows connect-with-timeout paths, so on failureerrstayed0. Both paths could also hand back a socket still in non-blocking mode, giving a caller expecting a blocking read a spuriousEAGAIN/WSAEWOULDBLOCKinstead of data; the POSIX case was worse, because an uncheckedF_GETFLmadeflags-1and that value was then restored onto the socket.SO_REUSEADDRand the twoSO_RCVTIMEOdefaults are now checked as well, deliberately non-fatal with the consequence recorded. Windows has noF_GETFLequivalent to get wrong —ioctlsockettakes an explicit mode — so it carried the same six defects minus that root cause - Editor hang on a child that had not written: the same unchecked-
F_GETFLshape inchild_run.hleftread_fdblocking, stalling the poll loop insideread(). The failure path now also reaps the already-forked child rather than leaving a zombie writing into a closed pipe - ONNX leaked
std::fixedonto every later float: both generation reports appliedstd::fixedwithstd::setprecision(1)directly tostd::wcoutand never restored it. This is user-visible, not cosmetic — the VM'sStdOutFloatPersets the stream's precision but never its floatfield, so nothing on the Objeck side could clear it, and every float printed after one SLM or vision call rendered in fixed notation at one decimal for the rest of the run. Both sites now format through a localwostringstream, sowcout's state is never touched and there is no save/restore to get wrong StdErrFloatsaved and restored the wrong streams: it read the saved format state fromstd::wcout, modifiedstd::wcerr, and restored ontostd::cout— three different objects.wcerrtherefore kept the fixed/scientific/precision setting permanently while narrowstd::couthad its flags overwritten- Exceptions escaping
main: every entry point had a large unprotected prologue —obi(all 89 lines) and the VM's POSIX main (all 126) had no handler at all, and the three that did started theirtrywell after the opening brace. That prologue is where the throws are: codecvt/locale construction in the REPL, and the longusage += ...wstringbuilding in every tool, which is abad_allocpath. Eachmainis now a thin wrapper around a staticobjeck_main. This introduced and then fixed a POSIX segfault: the VM's POSIX branch deliberately discardsExecute's result and fell off the end of the function, which is an implicitreturn 0inmainbut undefined behaviour in an ordinary function — at-O3no return instruction was emitted and control ran into garbage. Windows structurally could not catch it, since it takes the returning branch above - Negative
WriteBytesreturn converted tosize_t:SockDtlsOutByteAryandSockDtlsOutCharArypushedIPDtlsSocket::WriteBytes' return straight intoPushInt(size_t, ...), so the-1failure code reached the op stack through an implicit signed-to-unsigned conversion. It read back as-1on the Objeck side because the conversion sign-extends, but nothing in the code said that was intended - Unvalidated
/procarithmetic:GetProcessResidentBytescomputedpages * sysconf(_SC_PAGESIZE)from anfscanfof/proc/self/statm— a negative page count casts to a hugesize_t, the multiply can wrap, andsysconfreturns-1on error, which casts toSIZE_MAX. All three are checked, and the failure path leavesrssat0exactly as it already did - Malformed terminal escape sequences: bytes outside the ECMA-48 parameter ranges could be stored as negative characters before being fed to
atoi; they are now rejected the same way a short read already was
Performance
- Move instead of copy in the REPL and compiler entry points: clears eight
COPY_INSTEAD_OF_MOVEfindings. Stated honestly — these run once at startup on short strings, so the runtime saving is nil; the value is a clean baseline so a real defect in these files stands out in the next report. Each move was checked for a later use rather than applied mechanically:compiler/posix_main.cpp'susagedoes appear 22 lines later, in the mutually exclusiveelsebranch, and a mechanical application of the checker's advice would have needed exactly that check to be safe
Concurrency
free_memory_cache_sizeis atomic: mutated only underfree_memory_cache_lockbut read without it by the sweep and zeroed unlocked inInitialize. Nowstd::atomic<size_t>, relaxed throughout — the lock still orders the mutations, and the sweep's use is a heuristic threshold that tolerates a value one cycle stale- Sweep/mark lock hierarchy recorded: the reported
ORDER_REVERSALbetweenallocated_lockandpda_monitor_lockcannot fire today —CheckPdaRootsis joined before the sweep takes its lock, the JIT thread it spawns is joined inside it, andmarked_sweep_lockadmits one collector at a time. But the safety rested on four unstated invariants, and overlapping sweep with marking — the natural next step for this collector — makes the ABBA live and hangs the VM. The hierarchy is now documented at both lock sites half_to_float_u16rewritten with a signed exponent: the subnormal path relied on auint32_tunderflow wrapping back to exactly the right exponent field. Bit-identical after the change, verified by an exhaustive sweep of all 65,536 half patterns against an independentldexpreference, but the correctness no longer rests on a wrap the reader has to reconstruct
Release and Infrastructure
- Windows installers are signed, and the notes say so only when true: every Windows MSI from v2026.4.0 through v2026.8.2 shipped unsigned while the generated notes asserted “All Windows MSI installers are digitally signed for security”. This was not a missing secret —
signtoolwas configured, ran, and failed on every artifact, after which the build exited 0 by design and the publish workflow tolerated it. A false security claim survived a fully green pipeline for months. The signing certificate lives on a SafeNet eToken, so its private key is non-exportable and a GitHub-hosted runner has no USB port: CI signing is not broken, it is impossible. CI no longer attempts it, the generated notes no longer assert it, andcheck_release_signatures.ps1reports the realGet-AuthenticodeSignaturestatus per installer — alerting, printing the exactsign_release.cmdinvocation, and exiting non-zero when any installer is unsigned. It also warns when a signature is valid but not timestamped, since such a signature dies with the certificate rather than outliving it - Signing selects the certificate by thumbprint, not
/a:/apicks the “best” valid certificate, and the signing machine's store holds three valid code-signing certs with private keys. It was safe while the token was the only candidate; picking a self-signed cert would produce installers that carry a signature and fail every trust check — worse than shipping them unsigned - Coverity Scan on Windows: the first Windows scan captured the whole solution and found twenty real defects, most of them in cross-platform sources that MSVC compiles differently than GCC, so the Linux scan had never reached them. The scan token is read from the environment rather than the repository
linux-arm64is smoke tested: the POSIX smoke test excluded it on the assumption it was cross-compiled. It is not — the matrix builds it onubuntu-24.04-arm, which runs the binaries it produces — so the shippedlinux-arm64toolchain was the one platform never exercised before publication. Windows ARM64 is the only genuinely cross-compiled target and stays excluded
[v2026.8.2] - 2026-08-16
Supersedes v2026.8.0 and v2026.8.1, which were withdrawn; this entry covers everything since v2026.6.4.
New Features
- Subscript the result of a method call:
GetItems()[0]->GetName()now works. It had never been implemented, and the grammar swallowed the attempt rather than rejecting it —[after a call was read as the start of a fresh static-array literal, so the call was evaluated and discarded.GetArr()[0]->Size()silently returned1, the size of the literal[0], andGetArr()[0]->PrintLine()printed[0]; with an object-typed array the same root cause surfaced instead as "Undefined function/method call". The silent half is the dangerous one, since a wrong value is far harder to notice than a build failure. Implemented across the parser (all three call-parsing paths), the context analyzer (element-type resolution, index analysis, rejection of non-integer and multi-dimensional subscripts) and the emitter. Behaviour change: code that previously compiled and returned the wrong value now returns the correct one. AddHeadernow actually sends headers on HTTP/2 and HTTP/3:Http2Client->AddHeaderandHttp3Client->AddHeaderstored headers on the Objeck side and then dropped them — the trap signature had no headers argument, so they never reached the VM, and both native contexts carried arequest_headersmap the backends already consumed and nothing ever wrote. Headers now cross as aString[]of alternating key/value through newHTTP2_REQUEST_HDRS/HTTP3_REQUEST_HDRStraps and a 5-argumentRequestoverload. The existing 4-argument trap is deliberately untouched: theTRAPoperand encodes the argument count, only the JIT reads it, and CI does not rebuildlang.obl— so widening it would have desynced the operand stack silently, and only once the JIT threshold tripped. Verified by a server echoing the value back, not by a status code. HTTP/1.1 was never affected: it serializes headers in Objeck.- HTTP/3 on Windows:
Http3Clientnow works on Windows 11 / Server 2022 and later, backed by WinHTTP over MsQuic. It had shipped documented-and-dead: the trap handlers compile unconditionally, so the API was present and every request simply failed. The POSIX ngtcp2/GnuTLS path is untouched — none of it exists in the Windows toolchain — so this adds no vendored QUIC library, no second TLS backend and no new DLL to deploy (winhttp.libships with Windows). WinHTTP treats HTTP/3 as a preference and falls back transparently, so every request assertsWINHTTP_OPTION_HTTP_PROTOCOL_USEDand fails rather than silently downgrading — a client that quietly spoke HTTP/2 would be the same false promise this fixes. Verified live against four HTTP/3 hosts (GET, POST with body, custom headers, session reuse) and negatively against two HTTP/1.1-only hosts, which are correctly refused. runtime.feature.http3/runtime.feature.http2: report which protocol engines were actually compiled in. Because the traps compile unconditionally, "the API exists" proved nothing, and callers could not tell not supported from the network failed — which is how the Windows gap went unnoticed.Runtime->GetProperty("runtime.feature.http3")returns"1"only when an engine is really present.obuupdater, phase 2:obu updateandobu rollbackon Linux/macOS. It downloads the platform archive and itsSHA256SUMS, verifies the digest with an inline SHA-256 before touching disk, then does an all-or-nothing staged swap (.obu-work→.previous→ root) with anobc -vpost-install check and automatic rollback. No shell is used for the download, extract or swap — curl/tar/obc run via argument vectors — and a per-run lock plus interrupted-swap recovery guard against corruption. Covered by an offline CI test (test_update.sh). Windows in-place swap and signature verification are later phases.- Full-screen editor in
obi(/e): raw-mode terminal editing over the REPL buffer with damage-diffed rendering, CJK/tab-correct widths and a protected read-only shell. F5 compiles and runs the buffer as a subprocess, streaming its merged output into a pane without blocking the UI and F8 jumping the cursor through compile errors; undo/redo with coalesced typing runs, Shift-selection, an internal clipboard, and an opt-in vi profile (F2: hjkl, i/a/o, dd/yy/gg, visual mode)./etopens a terminal capability test. Header-only implementation; no build-system changes on any platform. obuupdater, phase 1:obu checkcompares the installed version against the latest GitHub release (or a pinned--channel <tag>), exit 0/1/2 with--quietfor scripting, using the system curl with no new dependencies. Every future release now ships aSHA256SUMSasset generated over the exact released file set, and a weekly workflow snapshots per-asset download counts into a committed CSV. Design indocs/UPDATER_DESIGN.md.- Debugger: structured variable inspection: every variable was previously reported with
variablesReference0, so an object, array or collection was a dead end in the Variables pane — aMapdisplayed asCollection.Map@0x1f4a2c0and nothing more. Objects now expand into their instance fields, arrays into indexed elements, andVector,MapandHashinto their contents (Mapin key order,Hashas key → value), each child expanding recursively. Strings and boxed scalars (IntRef,FloatRef, …) render as their value and are deliberately terminal, since drilling into them would only expose backing storage. Expansion handles hold raw object pointers, so they are discarded on every resume and on each new stop — the collector moves objects, and a handle must never outlive the stop that produced it. Depth, child-count and cycle guards keep a self-referencing structure from expanding forever. Collection internals are located by field name where debug symbols exist and by a validated slot index otherwise, because library classes are compiled without symbols and carry no declaration names. - Debugger: watch and hover expressions expand:
evaluatereturnedvariablesReference0, so a watch on a collection dead-ended even though the Variables pane could expand it. Expressions resolving to an object or array now hand back an expandable reference, backed by a new typed evaluation entry point (EvaluateForDapRaw) — the existing evaluator returned only a formatted string, with no value to attach a handle to. - Debugger: wider DAP protocol coverage (8 → 17 capabilities):
terminate(the graceful stop clients prefer overdisconnect),breakpointLocations(reports only lines that carry an instruction, so editors stop offering breakpoints that cannot bind — permissive until a program is loaded, since nothing can be validated before then),exceptionInfo,setExpression,completions(debug-console completion over the variables visible in the selected frame),modulesandloadedSources. Data breakpoints break when a value changes rather than when a line is reached; the watch machinery already existed for the CLIwatchcommand but reported only to the console and could not be created over DAP, so the stop now names the watch and reports its old → new transition. Variable paging is supported throughindexedVariablesplusstart/count, and the client's own capabilities are now read from the initialize request instead of ignored. - Unsigned integer literals: a
u/Usuffix reads a literal across the whole unsigned range and keeps its bit pattern, so0xFFFFFFFFFFFFFFFFuand18446744073709551615uare now writable. The suffix affects only how the number is scanned — the value is an ordinaryInt, so no new type, opcodes or conversion rules come with it. Hex and binary literals are read the same way, since they are bit-pattern notation. - Unsigned shift
>>>and unsignedIntoperations: every integer sits in a signed 64-bit slot, so>>copies the sign bit into the high end — there was no way to shift a value that is being used as a bit pattern rather than as a number.a >>> nshifts a zero in instead, at the same precedence as>>and*and left-associative. Alongside it,IntgainsShiftRightUnsigned,CompareUnsigned,DivideUnsigned,ModUnsignedandToUnsignedString, which read both operands as unsigned 64-bit quantities — so-1is18446744073709551615andInt->DivideUnsigned(-1, 3)is6148914691236517205. Like?->and??,>>>desugars in the parser onto an ordinary library call, so no opcode is added and the bytecode reader, both JIT backends and the VM are unchanged. BecauseVector<Vector<Vector<X>>>closes three type arguments with a single>>>token, the parser now tracks how many>a multi-character close still owes rather than a single "expand" flag; two- and three-deep generics are covered by the regression suite. - Language server reports
serverInfo: the initialize response carried no server name or version, so clients had nothing to display and a stale deployed server was invisible.codeActionProvidernow also declares itscodeActionKinds(quickfix,source.organizeImports) so clients can filter them and route "Organize Imports" to the server, andsignatureHelpProviderdeclaresretriggerCharactersto keep the signature popup alive while arguments are edited. - Nil-safe operators
?->and??:a?->b()callsbonly whenais non-Nil, yieldingNilinstead of faulting, anda ?? bsuppliesbwhenaisNil(evaluatingbonly when needed). A single?->guards the whole remainder of a chain, somaybe?->Trim()->ToUpper()is safe throughout. Both desugar in the parser onto the existingTry()/Otherwise()intrinsics —a?->b()isa->Try()->b()anda ?? bisa->Otherwise(b)— so no new opcode is emitted and the bytecode reader, both JIT backends, and the VM are unchanged. A chain ending in a value type needs??to supply the result, since anIntcannot itself beNil:maybe?->ToUpper()->Size() ?? -1. Note thatTry()guards against any runtime error in the chain, not only aNildereference. The spelling is?->rather than?.because Objeck's member accessor is->; it also avoids the float-literal ambiguity?.would introduce, since.followed by a digit starts a number.
Security
- HTTP request-line injection via a URL path:
Url->Newdoes not sanitize, and both the path and the host were appended verbatim into the HTTP/1.1 request line, so a CR/LF split the request line itself. Proven against a local listener: `Url->New("http://host/evil - Header injection via caller-supplied content types (HTTP/1.1 and HTTPS): the same flaw as the HTTP/2 and HTTP/3 case below, on the text-serialized path.
net.obsandnet_secure.obsappended the caller's content type verbatim at three sites, reachable from fifteen public methods, and the injected line landed beforeContent-Length— the request-smuggling position. Validated and failing closed at each serialization site. - HTTP request splitting via
AddHeader(HTTP/1.1 and HTTPS):net.obsandnet_secure.obsserialize the request as text in Objeck and appended caller-supplied header values verbatim at seven sites, so a CR/LF ended the field and began a new one. Demonstrated against a local listener, which receivedX-Injected: yesas a header of its own. Unlike the HTTP/2 and HTTP/3AddHeader— inert because headers never reached the VM — this one always worked, and so had always been injectable. AddsWeb.HTTP.HeaderCheck, guarding all sixAddHeadermethods per RFC 9110 5.1/5.5 as tightened by RFC 9113 8.2.1; it also rejects names beginning with:, since a caller-supplied pseudo-header is malformed and is the classic smuggling primitive. - Header injection via caller-supplied content types (HTTP/2 and HTTP/3):
Post/Put/Patchpassed the content type into the header serializer unvalidated. On Windows that builds a CRLF-delimited block forWinHttpAddRequestHeaders, whose input is a CRLF-separated header block — so `"text/plain - HTTP/3 connection IDs could be uninitialised stack memory: the five
gnutls_rndcalls in the QUIC client ignored their return value, and the initial connection IDs were declared uninitialised.gnutls_rndleaves the buffer untouched on failure, so a CSPRNG failure transmitted stack contents in the cleartext QUIC Initial header — a memory disclosure plus predictable connection IDs, and a forgeable stateless-reset token. All sites now check, the IDs are zero-initialised, connection setup fails closed, and the callback that cannot report failure returnsNGTCP2_ERR_CALLBACK_FAILURE. Secrets now draw fromGNUTLS_RND_KEY.
Bug Fixes
- Face detection never returned a single face:
FaceSession->Detectand->Recognizeproduced zero results for every image at every confidence threshold, on every platform shipping the ONNX native library, since v2026.5.3. Inference itself ran normally, which made it look like a model or threshold problem.scrfd_decodecompared a row count against an element count — each SCRFD output is[N, cols]andnis alreadyN, but bbox and keypoint tensors were tested againstexpected * 4andexpected * 10. At 640×640 stride 8 that is12800 < 51200, true for all three strides, so every stride group was skipped and the candidate list was always empty. Only the score tensor, withcols == 1, compared correctly, which disguised the error. Verified with the InsightFace buffalo_l models: detection now finds 2 faces in the sample group photo (scores 0.905 and 0.823), embeddings are 512-dimensional, and same-person / different-person similarity comes out at 0.767 and -0.023 respectively. Note this affects only the ONNX face API; YOLO and ResNet inference were unaffected. obuwas missing from every release archive: v2026.8.0 shipped the updater as a headline feature and delivered no binary.release-build.ymlnever referencedobu, and none of the three deploy scripts built or copied it, sobin/heldobb obc obd obi obron Windows, Linux x64/ARM64 and macOS alike. The source, its POSIX makefiles and its.vcxprojhad existed the whole time — only the packaging was absent. CI did not catch it becauseci-build.ymlexercisesobusolely throughcore/utils/updater/test_update.sh, which builds its own copy with test hooks: the updater was tested continuously while never being shipped.obuis now built and installed tobin/bydeploy_posix.sh,deploy_macos_arm64.shanddeploy_windows.cmd(x64 and ARM64), and both "Verify required binaries" gates inrelease-build.ymlnow list it, so a future omission fails the build before a tag is ever pushed.Web.HTTP.HeaderCheckdocumentation was largely invisible: the class landed in v2026.8.0 with onlyIsValidRequestTargetdocumented.IsValidNameandIsValidValue— the two functions guarding everyAddHeadercall — carried no doc comment at all, and the API index only records documented members, so LSP hover and completion showed nothing for the security fix's primary entry points. Separately,Flatten's doc block sat aboveIsValidRequestTargetrather than aboveFlatten, so it documented the wrong function and leftFlattenundocumented. All four functions are now documented and correctly bound; the index goes from 1 to 4 entries for the class.- ARM64 JIT clobbered a general register on every float compare-against-memory:
move_freg_fregemittedfmov d10, x{src}/fmov x{dest}, d10, addressing the general register file rather than the FP file, because theRegisterenum restarts float numbering atD0 = 0. Every call both failed to move the float and overwrote general registerX{dest}; when it held a live pointer, the next use followed a float bit pattern as an address — the long-open "memory corruption of free block" crash, whose PC0x3F947AE147AE147Bis the IEEE-754 bits of the double0.02. Correcting the encoding then exposed the real defect:cmp_mem_fregended with a copy that should never have been there, and since that path is reached from every JIT float division via the divide-by-zero guard, it copied0.0back into the divisor. One line, two bugs — inert-but-clobbering before, divisor-zeroing after. Removed, with the compare's operand order corrected to match AMD64 andcond_jmp/cmov. AMD64 was correct throughout. Guarded byprograms/regression/jit_float_mem_ops.obs, gating on both ARM64 legs. obdshipped with HTTP/2 and HTTP/3 compiled out:debugger.vcxprojcompiles the samecommon.cppas the VM but defined neither protocol macro, so a program that worked underobrfailed under the debugger. Same defect class as theHttp3Client-on-Windows gap, one level up.- HTTP/3 was silently unavailable on Linux ARM64:
Makefile.arm64never definedOBJECK_HAS_NGTCP2or linked ngtcp2/nghttp3/gnutls, soHttp3Clienthad no engine there even though CI installed the dependencies on that leg. Now enabled. HTTP/3 platform support is documented innet_quic.obs. - ONNX/phi3 native inference hardening: fixed an out-of-bounds read when a model exported mismatched KV key/value tensor counts (the generation loop indexed four parallel name arrays by a single layer count), guarded the logits-tensor rank/offset access, surfaced an unsupported logits dtype instead of silently sampling token 0, and made the token sampler safe on empty/degenerate distributions. The sampler moved to a dependency-free
phi3_sampling.hwith a unit test that runs in CI. - Primitive receiver operand order:
v->Pow(10)computedPow(10, v)-- 100 instead of 1024 -- whenever the receiver was a variable, instance variable or array element; literals were correct, so the two spellings of one call disagreed. All 29 multi-argument primitive functions were affected. The emitter now passes the receiver as the first argument for every receiver shape. - Calculated-expression receivers in expressions:
if((1 + 1)->Pow(10) = 1024)corrupted the heap -- the emitter dropped the call arguments, so the call consumed the comparison operand and the comparison ran on an empty stack. - Divide by zero at default optimization: the constant folder kept a zero-divisor division for its runtime trap but emitted it before its operands, turning the clean trap into a silent crash. Modulus by zero never trapped at all -- the interpreter had no zero check on
%-- and now traps exactly as division does. - Debugger String rendering: the DAP formatter read a String's backing-array capacity instead of its length, returning NUL-padded values on POSIX platforms; both debugger front ends now read the length from the String itself via a shared layout header (
obj_layout.h), which also ends the CLI/DAP drift that produced wrong collection sizes. - Integer literals past the signed range were silently wrong: the scanner parsed them with a saturating signed conversion and never checked its range error, so
99999999999999999999compiled as9223372036854775807with no diagnostic, and0xFFFFFFFFFFFFFFFF— an ordinary all-bits mask — produced the same saturated value instead of every bit set. Out-of-range literals are now rejected, and hex and binary are read as the bit patterns they denote. Behaviour change:0xFFFFFFFFFFFFFFFFnow evaluates to-1rather than9223372036854775807, matching C, Java, C# and Rust. Any hex literal at or above 2^63 changes value accordingly; none appear in the standard library. JsonElement->Encodeproduced invalid JSON: escapes were built as"\\u" + Int->ToHexString(), butToHexStringreturns a0x-prefixed, unpadded value — so U+2019 encoded as\u0x2019and U+00B2 as\u0xb2, neither of which is a legal escape. Every non-ASCII character was affected, and the defect was visible in a shipped artifact:tools/lsp/server/objk_apis.jsoncould not be read by a strict parser. Escapes are now four zero-padded hex digits with no prefix, and code points above the BMP are emitted as a UTF-16 surrogate pair.System.IO.ConsoleIO->Instance()never returned the singleton: the null check was inverted (<> Nilwhere= Nilwas meant), so the accessor returnedNilon the first call and allocated a fresh instance on every call after one existed. Callers survived only because the class holds no instance state, which madeConsole->Print/PrintLine(Bool)andReadLinework by luck.- Language server documentation index silently lost API entries:
objk_apis.jsonbacks LSP hover and completion docs, and a class that failed to parse was dropped with no error. A lone inline backtick in a doc comment terminated the description and failed the whole class; a bare~in prose ("(typically ~1.4)") did the same;ParseUsescanned to the next#, swallowing a followingbundledeclaration and filing every class in the file underDefault; method doc blocks containing a fenced example failed outright because only class comments handled fences; and the@hiddentag was unsupported, failing any block that used it. A trailing-comma bug also left the generated file invalid whenever the last bundle was empty. The index went from 368 to 379 entries with none lost, and parses as valid JSON for the first time. The generator's hard-coded source list had also drifted —concurrent.obs(all ofSystem.Concurrency) was missing and the deletedmath.obswas still listed — and it resolvedobcthroughPATH, so a system-wide Objeck install shadowed the build tree. - Formatter split
??into? ?: the LSP formatter's scanner tokenized?one character at a time, so formatting a file that used??emitted? ?and produced source that no longer compiled.??now scans as a single token. - Language server crashed on five requests:
textDocument/implementation,semanticTokens/full,inlayHint, and both call-hierarchy directions returned aResult[]through the local argument array and cast it on the way out, which fails at runtime and killed the server process (Invalid object cast: '?' to 'System.Diagnostics.Result'). Each now writes into anAnalysisinstance slot, the patternFindReferences/GetSymbolsalready used. - Requests resolved to the wrong method:
FindMethodOrClassconverted a method's start line to the editor's 0-based numbering but not its end line, and the end line records the token after the closing brace — so every method's range overran onto the next declaration and any cursor past the first method of a class resolved to the first method. This affected definition, references, hover, rename and call hierarchy.
Build
- Windows projects build on VS2022 again: every
.vcxprojhard-codedPlatformToolsetv145after the VS2026 migration, so the tree could only be built on a machine with VS2026 installed — a VS2022 box failed all 20 projects with MSB8020, anddeploy_windows.cmddrives its builds throughdevenv, which accepts no command-line override. Pinningv143instead was not an option either, since the CI runners ship onlyv145. The projects now select$(DefaultPlatformToolset), which resolves per machine.
Tooling
- The release build now proves the toolchain works: every gate before this asserted only that files existed, so a release could ship binaries unable to compile or run anything and still go out green. Both platforms now compile and execute a real program from the shipped tree and assert the reported version matches the tag. Relatedly,
deploy_windows.cmdre-runsupdate_version.ps1, which regeneratesversion.hfrom a hardcoded version — if it disagreed with the tag, Windows silently built a different version whose binaries then rejected the committed.oblset onVER_NUM, failing every program. The workflow now rewrites that file from the tag, and the release pre-flight refuses to proceed when the two disagree. - The six standalone DAP test suites (~78 assertions: drill-down, protocol, data breakpoints) now run in POSIX CI -- previously they ran only on Windows, and their first Linux/macOS run immediately caught the String bug above. The tail-call receiver gate from PR #578 is pinned by a regression test, as are all the fixes above.
- Debugger regression coverage: two new suites,
dap_drilldown_test.py(35 assertions over object, nested, array, object-array,Vector,Map,Hashand empty-collection expansion, scalars staying leaves, and handle invalidation across a resume) anddap_protocol_test.py(29 assertions over the new capabilities and requests). Both run fromrun_dap_tests.sh, which also gained the previously orphaneddap_stepout_types_test.py— it anddap_instance_var_test.pyhad never been wired into the harness, which is why the latter was able to rot unnoticed against a fixture whose line numbers had shifted. - Editor tooling is now tested in CI: a
toolsjob runs the formatter regression suite, a new LSP regression harness (tools/lsp/tests/run_lsp_tests.py, 25 assertions over the capability set and handler behavior), and the VS Code extension's TypeScript build and lint. The formatter suite had never run in this repository layout — itsOBJECK_ROOTresolved outside the repo — and the tooling scripts appended the build tree toPATH, so a system-wide Objeck install shadowed it.
[v2026.6.4] - 2026-06-28
Bug Fixes
- Multithreaded minor-GC crash during thread spawn: a spawning thread's
selfand method argument were held as raw pointers in a heapThreadHolderthat the moving collector neither marked nor relocated. A minor GC during the spawn handoff could promote/relocate the still-live object (kept reachable via another root, e.g. the parent's array) without updating the holder, so the new thread started with a stale young pointer — after the nursery reset that slot was reused, and the staleselfwas later dereferenced as a non-object (0xC0000005) inStackInterpreter::LoadClsInstIntVar. Fixed by tracking the holder's self/param slots in apending_thread_rootsregistry that the collector marks (CheckPendingThreadRoots) and relocates (FixupPendingThreadRoots) across every collection during the spawn handoff, registered inProcessAsyncMethodCalland removed at child teardown, on both the Win32 and POSIX thread paths. Intermittent; surfaced only under heavy multithreaded churn.
[v2026.6.3] - 2026-06-27
New Features
- Generational minor garbage collection: minor (nursery) collection is now enabled. A nursery-full collection scans only the remembered set plus roots and recycles the young generation without sweeping the old generation, falling back to a full major GC under old-generation pressure. JIT and interpreter reference stores emit the generational write barrier on both AMD64 and ARM64 (
allocation_sizeis now updated atomically), and the nursery is zeroed at allocation time rather than inside the stop-the-world pause. The young-generation allocator stops the world duringCollectMinorto fix a multithreaded young-gen corruption. - Closure ergonomics: function references gain three usability improvements — call a
FuncRefdirectly withv()instead of an explicit->Call(); write bare lambdas with an inferred return type (\(x) => x * 2) that auto-wrap intoFuncRef<R>when assigned, returned, passed as a method argument, or stored as a collection element; and give a lambda a block body (\(x) => { ... }). System.Concurrencylibrary (concurrent.obl): structured concurrency primitives —TaskScope,Task, andMonitor— plusruntime.*process/GC/CPU diagnostics (GC pause, promotion, allocation rate, lock contention, and thread/STW/nursery counters) read throughRuntime->GetProperty("runtime.…"). Now included in API-doc generation.
Bug Fixes
- Multi-lambda closure heap corruption: closures sharing a scope corrupted each other's captured state because captures were keyed by outer-scope ids; captures now use closure-local ids.
- Compiler: re-analyzed and repeated
FuncRefdirect calls, and functional-call result chaining in expression context, are now handled correctly. Closure-captured variables no longer raise a spurious "unreferenced variable" warning.
Performance
- SDL2 2D rendering:
Rendererdraws pool the boxing buffer and cache the proxy/method names, reducing per-call overhead.
Libraries / Infrastructure
- Removed the unused Gtk3 binding (
gtk3.obs). - Synced the Linux/Windows standard-library lists and rebuilt all
.obllibraries against the new toolchain version (VER_NUM 202663).
[v2026.6.2] - 2026-06-19
Performance
- GC safepoint poll made nearly free in JIT'd loops: the cooperative stop-the-world safepoint (added in v2026.6.1) emitted an unconditional
call MemoryManager::SafePointat every JIT label, which regressed label-dense integer loops. The poll is now (1) an inline flag test that calls into the collector only when a collection is actually active, (2) reads&stw_activefrom a callee-saved register cached at the prologue (R12 on AMD64, X19 on ARM64), and (3) emitted only at true loop back-edges rather than every label. Net:fannkuchreduxdropped roughly in half (~59s → ~31s in the Docker harness), recovering the full regression on both AMD64 and ARM64. Validated against the GC/JIT and multithreaded stop-the-world stress suites. - Auto-JIT for
DYN_MTHD_CALL(closure / function-reference calls) on AMD64 and ARM64: closure-heavy and function-reference kernels now JIT-compile once warm instead of staying in the interpreter.spectralnorm(input 5500) goes from 43s interpreted to 3.5s withOBJECK_JIT_THRESHOLD=1, and at n=2000 reaches 0.46s — matching the hand-nativekernel (0.40s). The remaining lever is auto-JIT threshold/warmup tuning, not coverage. - Inline young-generation bump allocation for
NEW_OBJ_INST(AMD64): nursery object allocation is emitted inline (atomic_fetch_add+ bounds check) instead of a call into the allocator on the hot path. - Interpreter float fast-path inlined directly in the dispatch loop, alongside the integer fast-paths added in v2026.6.0.
- ARM64 JIT whitelist parity with AMD64 so the same instruction set auto-JITs on both architectures.
Bug Fixes
- JIT float-codegen correctness (surfaced by forcing JIT with
OBJECK_JIT_THRESHOLD=1):- AMD64
Floor/Ceil/ArcTanfloat codegen corrected. - AMD64: two latent
DYN_MTHD_CALLmiscompiles fixed. - ARM64: transcendental/round ops read a
REG_FLOAToperand's register bookkeeping as a stack slot, producing garbage (e.g.atanreturned 0); fixed. - ARM64: a libc float-call result/argument was dropped when its holder register wasn't
D0(the GP-bridgemove_freg_fregswapped FMOV opcodes); now a truefmov Dd,Dn. This had divergedLogisticRegression(expin1/(1+exp(x))). - ARM64: pending working-stack integers are now spilled across an inlined float libc call whose
blrclobbers caller-saved temps (Sum5/Int->Pow). - ARM64:
imm19is masked (& 0x7FFFF) in the error-handler branch backpatch — a backward div-by-zero/bounds/null-deref branch was sign-extending over theb.condopcode and producing an illegal instruction (ml_gbtSIGILL). - AMD64 and ARM64: a TCO'd self-recursive tail call (e.g.
return Gcd(b, a%b)) stored into a slot before a deferredLOADof that slot was consumed, corrupting the value;ProcessStorenow materializes pending references to the slot first.
- AMD64
- ARM64 JIT negative-offset memory loads (SIGSEGV invoking a JIT-compiled closure captured in a collection, e.g.
Vector<FuncRef>->Get()then call): the ARM64 backend's memory encoders only emitted a scaled unsignedLDR, so a negative displacement — such as the second word of a 2-word func-ref loaded atop_stack[pos-1]— wasabs()'d and read the slot above the base, handing the lambda a garbageself. All ten ARM64 load/store encoders now route through one signed-offset helper that emitsLDUR/STURfor negative offsets (byte-identical to the prior encoding for non-negative offsets). amd64's displacements were already signed and were never affected. - VM shutdown thread race: worker threads are quiesced before program teardown, fixing a race during JIT program shutdown.
- UTF-8 in a non-UTF-8 locale:
obcfailed to read UTF-8 source andobrfailed to load/print UTF-8 strings when the process locale wasC/non-UTF-8. Replacedmbstowcs/wcstombswith systemic locale-independent UTF-8 codecs insys.h, plussetlocalefallbacks.
Libraries
Int->MinSize()now returnsINT64_MINinstead ofINT64_MAX(2->Pow(63)computed+2^63in float and saturated on float-to-int conversion; fixed to1 << 63).
Infrastructure
- Native cross-language perf gate (CI): a non-Docker harness (
perf-results/cross-lang/run_native.sh,perf-results/check_perf_gate.py,perf-results/perf_baseline.json,.github/workflows/perf-gate.yml) measures Objeck against Python/Ruby/LuaJIT/Java with committed baseline ratios, so performance regressions like the safepoint one are caught automatically. - Refactor:
Compile()flattened into early-return guard clauses. - Docs:
docs/performance.mdrefreshed with unified-Docker numbers on a 7950X3D and a prioritized speedup roadmap; static release badge added toREADME.md; deploy/bump skills hardened.
[v2026.6.1] - 2026-06-14
New Features
- String interpolation — expressions, format specifiers, and
String->Format:"{$...}"interpolation now accepts arbitrary expressions (arithmetic, comparison, logical — e.g."{$i + 1}","{$a * b - c}","{$x > y}"), not just bare variables and calls. Inline format specifiers in Python/.NET colon syntax control precision, width, alignment, and radix:"{$pi:.2}","{$n:5}"/"{$n:05}","{$s:<10}"/"{$s:>10}","{$v:x}"(hex),"{$v:b}"(binary). A new positional helperString->Format("{0} = {1}", a, b)(with{{/}}escaping) complements interpolation for reusable/runtime format strings. Backed by new library helpersFloat->ToString(value, precision)andString->PadTo(width, ch, is_left); specifiers desugar onto existing helpers with no new VM opcodes. - Generics: bounds, compound/F-bounds, and variance: type parameters gain
T : A & Bcompound bounds (a concrete argument must satisfy every bound), F-bounded constraintsT : Compare<T>, and declaration-site variance —out T(covariant) andin T(contravariant) — checked soundly in both directions and preserved across the.obllibrary boundary. Existing invariant generics and syntax are unchanged (outstays a usable identifier); generic type-mismatch diagnostics now render readable types (Hash<String, IntRef>). - Multithreaded stop-the-world garbage collection: the collector now coordinates safely across threads. Mutators poll safepoints in the interpreter dispatch loop and at allocation, park while a collection runs, and bracket blocking syscalls (thread join/sleep, socket I/O) so a stop-the-world pause can always proceed; the AMD64 and ARM64 JITs emit safepoint polls at every label. Validated on Windows, Linux, and macOS across x64 and ARM64.
- Reproducible library builds: compiling unchanged
.obssource now produces byte-identical.obloutput. Anonymous classes are named from their source location instead of a random token, and string-selectcases and closure declarations are emitted in a stable (source/mthd_id) order rather than heap-pointer order, so committed libraries no longer churn on every rebuild. - Debugger (
obd) — major CLI and DAP expansion: the command-line debugger gains frame navigation (frame [n]/up/down, so you can inspect a caller's locals — theframecommand was previously a no-op) andlocals(print every local in the selected frame at once);set <var> = <expr>to change a liveInt/Char/Float; breakpoints by method (b Class->Method), temporary one-shot breakpoints (tbreak), and per-breakpointenable/disable/ignore <count>with ids shown bybreaks; data breakpoints (watch <expr>/watches/unwatch) that stop when a value changes;until <line>run-to-line; an empty Enter repeats the previous command; and breakpoints set on a non-executable line are now relocated to the nearest executable line with a note instead of silently never firing. The DAP adapter (VS Code) addssetVariable(editInt/Char/Floatlocals from the Variables pane), function breakpoints, logpoints ({expr}-interpolated log-and-continue messages), in-process restart, and exception breakpoints (break on an uncaught Objeck runtime error). A 24-check CLI suite and a 14-check DAP-protocol suite cover all of it across every platform.
Security
- TLS server-certificate verification is now on by default: secure client sockets and DTLS connections verify the server certificate chain. Self-signed certificates for local testing can be allowed with
OBJECK_TLS_INSECURE_SKIP_VERIFY=1. - Hardened untrusted-input deserialization: object deserializers reject hostile 64-bit sizes, a
Char[]read trap no longer lets an attacker-controllable offset overflow the heap, and additional untrusted-input paths were hardened against memory corruption.
Bug Fixes
- GC value corruption under heavy thread churn: a thread executing its top-level method (or holding an object only on a parked thread's operand stack) could have those live references missed by the mark phase and reclaimed mid-use. The mark phase now scans the top-level frame and every thread's operand stack, exactly matching what the fixup phase rewrites. Also: the young-generation bump pointer is bounded with a compare-and-swap so a collection can't run off the region, a thread's GC roots are deregistered before its stacks are freed (no use-after-free during teardown), the JIT join/sleep paths park correctly, and the write-barrier flag is accessed atomically on weakly-ordered ARM64.
- Serialization correctness: integer arrays dropped half their elements; object-nested integer arrays truncated 64-bit values to 32 bits; object function-reference fields desynced and lost data; several further 64-bit /
Char[]/Float-slot serialization bugs were fixed. - Compiler: constant propagation emitted a stale literal after a non-constant reassignment; LICM hoisted trapping
DIV_INT/MOD_INTout of loops that may never execute. - Debugger: locals and fields appearing after a
Floatfield/local showed wrong values. CLI conditional breakpoints (b file:line if <expr>) never parsed — the scanner downgraded theifkeyword to an identifier, so every conditional breakpoint failed with "Expected statement end". In DAP, variables in single-slot methods (e.g. a one-parameter recursive method) were all reported as<out of scope>because the bounds check divided a slot count bysizeof(size_t). TheMemoryManageris now idempotent on teardown and re-initializes on reload, so the debugger can re-run a program in-process (DAP restart) without a double-free. - macOS / launcher: portable application bundles failed when launched from outside their own directory; a stale
Objeck.appbundle version was corrected.
Performance
- ONNX on macOS: compiled CoreML models are persisted across runs (~35× faster warm start), and
Ort::TypeInfois kept alive while reading tensor type/shape.
Documentation / Infrastructure
- AMD64/ARM64 JIT and VM/memory-manager architecture READMEs expanded;
architecture.mddocuments the cooperative multithreaded stop-the-world model. - Build: resolved
D9025/LNK4099/LNK4098warnings and aNativeCodeODR violation. - CI: Windows runners pinned to
windows-2022(VS2022 toolset v143).
[v2026.6.0] - 2026-06-07
New Features
System.AIlibrary (-lib ai/@ai): classic AI in the standard library — graph search (Dijkstra,AStar,BreadthFirst,DepthFirstover a shared best-first core), adversarial game search (Minimaxwith alpha-beta pruning,MonteCarloTreeSearch), metaheuristics (GeneticAlgorithm,SimulatedAnnealing,HillClimbing), and tabular reinforcement learning (QLearning,Sarsa,MarkovDecisionProcessvalue iteration); all stochastic algorithms are seedable for reproducible runsSystem.MLoverhaul: 13 new estimators —RidgeRegression,LassoRegression,ElasticNet,Perceptron,SVM,PCA,GaussianNaiveBayes,AdaBoost,DBSCAN,GaussianMixture,KDTree,RegressionTree,GradientBoostedTrees; real recursiveDecisionTreeand votingRandomForest;KMeansk-means++ seeding, iteration cap, and empty-cluster handling;NeuralNetworkhidden/output bias vectors (clean XOR convergence);LinearRegression/LogisticRegressionintercepts, stable sigmoid, L2 regularization, andScore; seedableSystem.ML.Random; uniformFit/Predict/Score/IsFitted/Store/LoadAPI across every estimator;ml.obssplit into seven thematic source filesrecordtypes:record Point { @x : Int; @y : Int; }generates the constructor and accessors;record : readonly :omits setters and the compiler rejects field assignment outside constructors; supports generics, inheritance, and user-defined member overrides- Tail Call Optimization (TCO): self-recursive tail calls rewritten to jumps, eliminating stack growth (
-opt s1+) - Loop-Invariant Code Motion (LICM): hoists
arr->Size()reads and pure arithmetic out of loop bodies (-opt s2+)
Breaking Changes
RandomForest->Trainis nowFit(uniform estimator API)- Stored
NeuralNetworkmodel files must be regenerated (serialized format gained bias vectors)
Bug Fixes
- VM/JIT frame-dependent traps: traps reading interpreter locals (
Serializer->Write,Date->New, file-time queries) crashed once a method crossed the auto-JIT threshold; such methods now stay interpreted on AMD64 and ARM64 - ARM64 JIT: stale
selfreload after JIT-to-interpreter callbacks; JIT-to-JIT errors are now diagnosable; operand-kind compile guards ported from AMD64 - Float equality on array elements: was compiled as an integer compare in the JIT
- Bool array literals: every bool static-array literal after the first silently received the first literal's data (broken literal-pool comparator); literal dedup now works for all array types; array dimensions capped at 8 with a proper diagnostic
- Launchers: Windows defect sweep; macOS version-check modernization
- Doc parser: inline backticks and bare tildes in descriptions no longer break API doc generation
- XML parser: truncated documents (elements left unclosed at end of input) and stray closing tags were silently accepted; both now fail cleanly.
XmlElement->DecodeStringdropped the trailing semicolon when decoding'
Libraries
- Data.XML conveniences (additive; raw
SetContent/GetContent/GetValuecontract unchanged):XmlElement->EncodeTextescapes exactly the five markup characters while leaving whitespace readable;SetEncodedContentproduces well-formed output;GetDecodedContentandXmlAttribute->GetDecodedValuereturn entity-decoded text
Performance
- Auto-JIT now compiles methods containing
MTHD_CALLafter 10 invocations (5–15% speedup across benchmarks) - Interpreter fast-path extended with 15 additional inline opcodes (comparisons, bitwise, shifts, logical)
bench_matrix_multiply−14%,bench_dead_code−15%,bench_array_intensive−12%,binarytrees−7%,mandelbrot−6%
Documentation / Infrastructure
- Library aliases documented:
-lib @std/@ml/@gameand the new@aigroup, user-editable vialib/configobjk.ini; AI/ML developer guide gainsSystem.MLandSystem.AIsections with runnable examples - API reference regenerated (438 pages;
System.AIclasses grouped correctly);architecture.mdmermaid diagrams fixed and the GC description corrected (young generation moves survivors) - ARM bootstrap (
update_version_arm.sh) builtopencvbefore itsjsondependency — reordered so a clean ARM bootstrap succeeds - CI hardening: vcpkg installs retry on transient CDN failures;
mcp_server_testvalidates JSON-RPC bodies before accepting; flaky network tests quarantined with failure observability; regression timeouts added
[v2026.5.4] - 2026-05-28
Bug Fixes
.obe/.oblformat detection: correctly handles the edge case where a new-format size-header LSB collides with the0x78zlib CMF byte (fixed Windows CI debugger tests)- LSP shell script permissions: all
tools/lsp/shell scripts now have the execute bit set in git, fixingPermission deniedin release CI
Infrastructure
- Release workflow:
git checkout -f masterprevents a dirty-tree abort when committingapi.zipfrom a tag-based build
[v2026.5.3] - 2026-05-24
New Features
- Three-tier
selectdispatch (AMD64 + ARM64 JIT): single-caseselectcompiles to a direct compare-and-jump; 2–5 integer cases use a linear scan; 6+ dense integer cases emit a native O(1) jump table (JMP_TABLE/JMP_TABLE_SLOTopcodes); sparse or stringselectuses a binary search tree — matching the fastest dispatch strategy for each shape automatically.
Bug Fixes
- Fixed
HttpRequestHandlerandHttpsRequestHandler:ReadLine()can returnNilon a dropped or errored connection; calling->Size()onNilproduced a SIGSEGV in the MCP server and any HTTP server that receives an abrupt client disconnect before sending a request line. - Fixed
String->Split(Char): last token was sliced using@string->Size()(array capacity) instead of@pos(logical string length), producing an oversized trailing token on strings that did not fill the backing array. - Fixed
bench_spectralnorm_nativebenchmark: allocating arrays inside anativeJIT function caused op-stack imbalance during nested JIT-to-interpreter callbacks, producing a garbage result (~3.84e-156 instead of ~1.274). - AMD64 JIT trig:
sin,cos,tan,asin,acos,atan,atan2,sinh,cosh,tanhwere using x87fsin/fcos/ftan— replaced withcall_xfuncto use the C runtime (consistent with ARM64 and the LOG/EXP fixes) - AMD64 JIT float input:
REG_FLOATas the source operand ofcall_xfunc/sqrt/roundcaused a crash due to incorrect register state when a float value was loaded from memory immediately before the dispatch - Inline optimizer jump tables:
InlineMethoddid not shiftJMP_TABLE/JMP_TABLE_SLOTlabel operands byjump_inline_offsetwhen inlining methods containingselectjump tables, causing every slot to resolve to ip=0 CleanLabelsLocationend-of-stream: consecutiveLBLnodes at the very end of an instruction list read one instruction past the end of the streamCanInlineMethodconflict check: used theJUMP_OFF_INCconstant instead of the actualjump_inline_offsetaccumulator, producing false-positive label conflicts that unnecessarily blocked inliningString->SubString: crash on negative or zero length argument (#534)
Security / Performance
- Binary file integrity:
.obe/.oblfiles now store the uncompressed size as a 4-byte header before the zlib stream — eliminates the allocation guessing/doubling loop on load and lays the groundwork for future integrity checks - Switched from
compress()(level 6) tocompress2()atZ_BEST_COMPRESSION(level 9) — ~10–15% smaller binary files at no runtime cost - Replaced
callocwithmallocinCompressZlib/UncompressZlib— removes wasteful zero-initialisation of buffers that are immediately overwritten by the codec - Backward-compatible: files in the old raw-zlib format (CMF byte
0x78) are automatically detected and continue to load without recompilation
Performance
bench_spectralnorm_native: rewroteMultiplyAv/MultiplyAtvwith an incremental floating-point denominator, eliminatingI2Fconversions from the inner loop (2000×2000×40 iterations). Only two integer-to-float conversions now occur per outer row instead of per element.
Infrastructure
- Consolidated the
objeck-lsprepository intotools/lsp/— LSP is tightly coupled to each toolchain build and must be updated with every release - Rewrote CI
build-lspjob: Ubuntu runner, builds Linux x64 toolchain, compilesobjeck_lsp.obeviabuild_server.sh, packages VS Code extension withvsce, assembles versionedobjeck-lsp_VERSION.zip - Added
publish-vscodeCI job: publishes the VS Code extension to the marketplace on release using theVSCE_PATsecret build_server.sh/build_server.cmd:OBJECK_ROOTis now configurable via environment variable (defaults to../../..relative totools/lsp/server/)
[v2026.5.2] - 2026-05-17
New Features
- HTTP/2 client (
-lib net_h2): NewHttp2Clientclass for persistent multiplexed TLS connections using ALPN h2 via nghttp2. Supports GET, POST, PUT, DELETE, PATCH andQuick*static convenience methods that accept aUrlobject for one-shot requests. Linux, macOS, and MSYS2. - HTTP/3 client (
-lib net_quic): NewHttp3Clientclass for QUIC connections via ngtcp2 + GnuTLS + nghttp3. Same API asHttp2ClientwithQuick*statics for one-shot use. Linux and macOS only. - OpenAI Moderation:
Moderation->Check()returns per-category flags and confidence scores. - OpenAI Batch:
Batch->Create()/Get()for async 50%-cost batch requests (up to 50k at a time). - Gemini Files API: upload, list, get, and delete files via
FileManager. - Gemini Context Caching:
CachedContent->Create()for server-side prompt caching with configurable TTL. - Gemini Search Grounding:
Model->GenerateContentWithGrounding()anchors responses in live Google Search results. - Gemini Batch Embeddings:
Model->BatchEmbedContent()embeds multiple texts in one round-trip. - EmbeddingValues wrapper: avoids
Float[]as generic type parameter. - Socket options: New methods on
TCPSocketandTCPSecureSocket—SetKeepAlive,SetNoDelay,SetRecvTimeout,SetSendTimeout,SetRecvBufferSize,SetSendBufferSize,SetConnectTimeout.TCPSecureSocketaddsSetMinTLSVersion,SetVerifyPeer,GetCertFingerprint. SO_REUSEADDRonTCPSocketServer::Bind()survives TIME_WAIT restarts;IPSocket::Open()falls through to next address onsocket()failure.
Bug Fixes
- Fixed
obr(VM executable) absent from all platform archives —libnghttp2-devwas missing from the mainbuildCI job; equivalent gaps on macOS and Windows - Fixed
HttpsClientandHttpClientredirect not following POST/PUT bodies - Fixed HTTP/1.1 redirect handling for POST and PUT; added retry-on-reset parity across verbs
- Fixed 8 WebSocket bugs; replaced per-byte reads with bulk
ReadBufferI/O - Fixed MCP server hang on shutdown and crash-on-stop
- Fixed MSVC compatibility: NOMINMAX ordering,
std::min()ambiguity; Release VM/compiler optimizations - Fixed Realtime API null-safety issues
- Fixed ARM64 Windows ONNX
.vcxprojconfigs (Release-QNN|ARM64,Release-DML|ARM64); removed non-existentonnxruntime_providers.libfrom link deps
Libraries
- HTTP/2 (
net_h2):Http2Client— persistent HTTP/2 session with full verb support and URL-basedQuick*convenience functions. - HTTP/3 (
net_quic):Http3Client— QUIC-based HTTP/3 session with full verb support and URL-basedQuick*convenience functions. - HTTP/1.1 (
net,net_secure): AddedHttpClient->PATCH,HttpsClient->PATCH. Fixed redirect handling for POST and PUT.
Infrastructure
- ARM64 Windows OpenCV: switched to split module libs via vcpkg; corrected
Release|ARM64andDebug|ARM64.vcxprojconfigs - Committed
nghttp2headers and import libraries tocore/lib/nghttp2/win/; Windows builds are now fully self-contained without vcpkg - Multi-level NuGet restore: VS-bundled
nuget.exe→ PATH (Chocolatey on CI runners) → download from nuget.org - Added HTTP/3 dependencies (
libngtcp2-dev,libngtcp2-crypto-gnutls-dev,libnghttp3-dev,libgnutls28-dev) to Linux and macOSrelease-build.yml; added ngtcp2 GnuTLS backend build step for macOS - Hardened
deploy_windows.cmdwith artifact existence checks after eachdevenvbuild - Added binary verification CI step:
obr,obc,obd,obi,obb+ 50+ API HTML files must exist before artifact upload - CI: CodeQL v4, node24-compatible Actions, nghttp2/ngtcp2 on all platforms
net_h2.oblandnet_quic.oblcompiled as part of the standard library build inupdate_version.sh- macOS Xcode project: added
OBJECK_HAS_NGTCP2flag and-lngtcp2 -lngtcp2_crypto_gnutls -lnghttp3 -lgnutlslinker flags
Website
- Added Changelog card to home page (
docs/web/index.html) creating a clean 2×3 six-card grid
[v2026.4.3] - 2026-04-12
New Features
- DAP debugger hover: Hovering an object shows
ClassName { field=val, ... }with one-level instance field expansion viaFormatObjectForDap - DAP instance/class variable scopes: Variables pane now shows separate Locals, Instance, and Class scopes with correct memory mapping
- Configurable JIT threshold: Auto-JIT invocation count can now be tuned at startup
- Editor setup refresh: Updated VS Code, Sublime Text, and gvim/Vim DAP+LSP setup for Windows, Linux, and macOS with per-platform instructions
Bug Fixes
- Fixed DAP step-into crash, step-over/step-out scoping, and disconnect access violation
- Fixed DAP stdout/stderr corruption — redirected program output through capture pipes so DAP protocol stream stays clean
- Fixed DAP variable display, source path resolution, and pipe crashes
- Fixed DAP variable memory mapping to match CLI debugger
- Fixed LSP server crash on
textDocument/codeActionwith inferred locals - Fixed LSP hover position conversion from 1-based to 0-based
- Fixed JIT S2F callback param count causing segfault on
String:ToFloat - Hardened HTTPS client against null
ReadLineon connection failures
Infrastructure
- DAP integration test suite (
programs/regression/run_dap_tests.sh) - Removed redundant
HandleEvaluatefallback in DAP adapter (consolidated intoEvaluateForDap)
[v2026.4.2] - 2026-04-06
New Features
- JIT local variable register cache (AMD64 + ARM64): Keeps values in registers after store, avoids redundant reloads, evicts on demand when register pool is exhausted — ~3x speedup across all benchmarks
- DTLS (Datagram TLS) support: New
DTLSSocketandDTLSSocketServerclasses for secure UDP communication (IoT, VoIP, gaming) - Editor guide: New
docs/editors.mdwith Vim, Emacs, Sublime, Neovim, and DAP debugging setup
Libraries
- Gemini API: Added 2.5 Pro/Flash model constants, system instruction support
- Ollama API: Configurable host, Options class (temperature/top_p/top_k), Tool class for function calling
- OpenAI API: New
Embeddingclass,Modelsconstants (GPT-4.1, O3, O4-mini) - ML library: Fixed EuclideanDistance/StdDev bugs, added
LinearRegressionandLogisticRegression - Hardened JSON, JSON stream, and XML parsers against malformed input
- Hardened HTTPS client against null ReadLine on connection failures
Performance
- Link-time optimization: Added
-flto=autoacross all GCC Makefiles (AMD64 and ARM64) - ARM64 native CPU tuning:
-mcpu=nativeauto-detects RPi5 (Cortex-A76) and Jetson Orin (Cortex-A78AE)
Bug Fixes
- Fixed all MSVC and GCC compiler warnings
- Fixed doc generator error on
@hiddentag
[v2026.4.1] - 2026-04-05
Changes
- Fixed ONNX API documentation: trimmed bundle-level doc comment that was dumping code examples into the API page description
- Fixed release pipeline: macOS .pkg staple non-fatal, Windows ARM64 signing skip (cross-compiled), temp directory creation
- Rebuilt libraries with v2026.4.1 version
[v2026.4.0] - 2026-04-05
New Features
- Debug Adapter Protocol (DAP): Full VS Code debugging support with breakpoints, stepping, variable inspection, and stack traces
- Conditional Breakpoints: Break on expressions (e.g.,
b file.obs:30 when count > 5) - Debugger ANSI Colors: Syntax-highlighted source listing with color-coded breakpoints, current line, and line numbers
- Readline Support: Command history and line editing in the interactive debugger
- macOS .pkg Installer: Signed and notarized native package installer with PATH auto-configuration
- SSE Streaming: Server-Sent Events support for HTTP client/server
- ODBC BigInt & Connection Strings: Extended database connectivity with BigInt type and connection string authentication
Performance Optimizations
- 3.3x binarytrees speedup: Young-gen bump allocator, direct JIT-to-JIT calling, atomic CAS mark bits
- MTHD_CALL JIT whitelist (x64 + ARM64): Methods containing method calls can now be JIT-compiled
- GC thread safety: Memory barriers in PushFrame/PopFrame paired with acquire fences in GC marking — fixes intermittent threading segfaults on Linux
- JIT instance method inlining fix: Save/restore INSTANCE_MEM around inlined code
Networking & I/O
- Socket default receive timeouts to prevent hung connections
- Fixed ReadBytes partial read bug (short reads no longer silently truncated)
- HTTP client/server stack hardening with loopback regression tests
Libraries
- OpenCV: Contours, VideoWriter, transforms, normalization, 15 new image processing functions
- ODBC: Transactions, error handling, schema discovery, BigInt support
- ONNX: Phi-3 Vision multimodal inference, unified build system (DML/CUDA/QNN/CoreML)
- Collections: Hash auto-resize at 75% load, Vector in-place Remove
- JSON: Escape and keyword parsing fixes
CI/CD & Infrastructure
- Apple code signing: Developer ID Application + Installer certificates with notarization
- Windows code signing: Sectigo certificate with SHA-256 timestamping
- CI library rebuild on all platforms (not just bootstrap)
- Threading test retry logic for CI runner timing sensitivity
- 14 debugger regression tests with expect-based automation
- HTTP loopback and network buffer regression tests
Bug Fixes
- Fixed constructor early return crash
- Fixed CSV.Median, CSV.Average, URL encoding, Response.ToString nil check
- Fixed debugger list command corrupting Windows console on Unicode source files
- Fixed OpenCV Xcode header paths (ABI namespace mismatch with Homebrew)
[v2026.2.1] - 2026-02-26
New Features
- Try/Otherwise Error Handling:
Try()andOtherwise()compiler intrinsics on theBaseclass for safe method chaining with graceful error recovery (e.g.,obj->Try()->Method()->Otherwise(default))
Performance Optimizations
- 4.38x speedup on nbody benchmark via increased method inline limit (128→256 bytes), allowing getter/setter-heavy code to be fully inlined and JIT-optimized
- Common Subexpression Elimination (CSE): New compiler pass at s2+ eliminates redundant computations within basic blocks
- Dead Code Elimination: New compiler pass removes redundant jumps to immediately-following labels
- Bug Fix: Division-by-zero crash in constant folding —
DIV_INT/MOD_INTnow guards against zero divisor - Bug Fix: Dead condition in
InstructionReplacement—&&corrected to||(both types could never match simultaneously) - JIT Safety: Division-by-zero guards added to
ProcessIntFoldin both x64 and ARM64 JIT backends - Fixed Try/Otherwise crash: VM segfault when calling non-virtual methods (e.g.,
Size(),ToUpper()) on Nil objects insideTry()chains — call stack and instruction pointer now properly unwound during error recovery - Fixed debugger build error on Windows:
HELP_COMMANDenum collision with Windows SDK macro (WinUser.h) - Benchmarks: 10 new performance benchmark programs with measurement tooling (details)
Improvements
- Editor Syntax Highlighting: Updated Monaco (playground) and VSCode syntax definitions to support
TryandOtherwiseas built-in keywords - Web Playground: Updated version tag and editor keyword support for v2026
- ARM64 CI Testing: Enabled Linux ARM64 (
ubuntu-24.04-arm64) and macOS ARM64 test execution in GitHub Actions - Documentation: Bootstrap & cross-platform build workflow documented in
core/readme.md
Internal
- Refactored Try/Otherwise from expression-level keywords to method intrinsics on
Baseclass for cleaner semantics - Removed disabled legacy CI workflow (superseded by
ci-build.yml) - Refactored
OptimizeMethod()withRunPasshelper for cleaner pass management
[v2026.2.0] - 2026-02-12
New Features
- NLP Library: Comprehensive natural language processing with tokenization, TF-IDF, similarity, and sentiment analysis
- Web Playground: Browser-based Objeck playground at playground.objeck.org
- Gemini 2.0/2.5 Support: Google Gemini integration with audio capabilities
- OpenCV Integration: Real-time computer vision (face detection, image/video processing)
- OpenAI Realtime API: Support for
gpt-4o-realtime-previewwith text and audio - PCM16 Audio: Recording and playback APIs via SDL2 mixer
- Audio Conversion: PCM16 to MP3 via Lame
- ONNX Runtime: Cross-platform ML inference support
- Modern CLI Flags: GNU-style flags (
--source/-s,--destination/-d,--debug/-D) with full backward compatibility
Performance Optimizations
- Memory Manager: O(1) lookups via
std::unordered_set, in-place sweep, eliminated unnecessary thread creation - ARM64 JIT: 11 critical fixes including multiply optimization, register targeting, FP register pool management
- x64 JIT: Instruction encoding optimizations with dynamic backpatching
New Platform Support
- Windows ARM64: Full support with automated builds and code signing
- Crypto Library: Migrated from OpenSSL to mbedTLS for lighter footprint and ARM64 support
Collections and String Improvements
- String: New methods (
Contains,Count, character checks) and critical bug fixes (Trim,TrimFront,Set) - Collections: 10+ bug fixes in
gen_collect.obs(memory safety, iterator API, tree corruption, type issues)
CI/CD and Infrastructure
- Fully automated release pipeline via GitHub Actions (build, sign, publish)
- Automated regression testing on every commit (350+ tests)
- Windows MSI code signing with timestamping
- Cross-platform CI for Windows x64/ARM64, Linux x64, macOS ARM64
Compiler Improvements
- Better error messages with operator symbols, normalized type names, inline hints
- Fixed method chaining on array-indexed elements after cast (#524)
Bug Fixes
- String: Fixed
ToString()buffer size for Int and Float (returned buffer capacity instead of content length) - ARM64 JIT: Fixed STRH/LDRH opcodes, memory offset scaling, bitwise NOT
- Memory Manager: Fixed root scanning, race conditions, redundant binary search
- Crypto: Fixed AES-256 CBC padding and key derivation
[v2025.7.0] - 2025-07-01 ✅
Added
Hash->Dict(..)method to collectionsMap->Dict(..)method to collectionsVector->Zip(..)method to collections
Changed
- Updated documentation style
- Updated logos and branding
- Improved visual design
Fixed
- Various bug fixes and stability improvements
Previous Releases
For releases prior to v2025.7.0, please see the commit history on GitHub: https://github.com/objeck/objeck-lang/commits/master
Versioning
Objeck uses calendar versioning (CalVer) with the format YYYY.MINOR.PATCH:
- YYYY: Year of release
- MINOR: Minor version number (incremented for feature releases)
- PATCH: Patch number (incremented for bug fixes)