After shipping the optimizing compiler in v1.7.0, I returned to questions that sound less advanced: who keeps an environment alive, what a closed Stream still retains, who collects child processes after a task fails, and whether an update has actually committed when the caller receives an error.
These questions are less visible than new syntax. They determine whether I can trust HHY to keep running and to modify real application state.
My previous article followed the path from resource lifetimes to HIR/MIR. v1.7.1 continues that work by repairing ownership, defining several kinds of commit, and establishing a performance baseline with the current binary.
The most important progress, for me, is giving success, failure and an unknown outcome distinct meanings. Shared state, coroutines and distributed transactions will all need that foundation.
This article draws on the September 14, 2026 language report and archived release evidence. The performance table uses that report's measurements; I did not rerun benchmarks while writing. Future directions follow the public roadmap's 1.x, 2.x and 3.x series as of the same date. They are plans, with no calendar commitment.
What v1.7.0 established, and what v1.7.1 takes on
v1.7.0 shipped structured HIR, six static optimization passes, bounded integer MIR, type-feedback guards and deoptimization, and scalar replacement for eligible local integer Lists. That pipeline moves some repeated work into compilation and subjects the result to an independent Verifier.
Shipping it does not mean every application becomes faster by default. Direct Bytecode remains the default, with AST as an independent semantic reference. HIR, feedback specialization and scalar replacement remain opt-in. Scalar replacement also preserves the original GC and quota reservations; it does not establish that heap allocation has been eliminated.
For v1.7.1, I focused on the layer closer to applications. HHY now has files, HTTP, processes, Streams, a persistent Web Runtime and a database extension. A call can lend out a cursor. A lazy pipeline can retain a closure environment. A failed parent task can leave behind external writes that already succeeded.
The Runtime needs to explain those lifetimes before more fast paths make them harder to follow.
Repairing resource ownership first
Explicit GC roots for Context environments are an important part of this release. Environments needed by repeated host calls must remain alive through collection. Candidate state, transactions and strict scopes also participate in root management and exceptional-exit cleanup.
Closing a Stream now releases payload, environment, callback and jobs references. Registry chain nodes remain to protect checkpoints. Describing this as immediately deleting every resource registration would hide the actual lifetime design.
Task child-process failure recovery and the portable no-replace fixes address the same responsibility: every state established on the successful path needs an owner when execution exits early.
Memory accounting also needs a precise description. max_memory limits the language heap relative to the Runtime startup baseline. It is neither process RSS nor total memory across all Workers. To assess a persistent service, I still need RSS, file-descriptor, child-process and temporary-file observations over time.
Seven experimental APIs, four responsibilities
The seven controlled-concurrency APIs in v1.7.1 are disabled by default:
HHY_CONCURRENCY_EXPERIMENTS=1 hhy run app.hhy
They cover state publication inside a Runtime, cooperative updates to one file, strict database transactions and supervision of finite task batches.
| Scope | APIs | Responsibility |
|---|---|---|
| Single-owner state | atomic_state, atomic_read, atomic_update, atomic_close | Validate a candidate and publish the complete value once |
| One file | atomic_file_update | Read, modify and replace under one cooperative lock protocol |
| Database | transaction_strict | Bind constrained callback operations to one transaction; prevent commit after failure |
| Bounded tasks | task_map | Isolated child processes, ordered results, failure supervision and direct-child cleanup |
These interfaces do not make HHY a shared-memory concurrency language. Its current model combines a synchronous Runtime, isolated processes, bounded parallelism and external storage transactions. There is no new coroutine scheduler or async/await.
I want each guarantee to start with a resource it can actually govern. Atomicity becomes useful when its scope is explicit.
Atomic state publishes related fields as one candidate
Suppose two fields need to change together:
let state = atomic_state({left: 100, right: 0})
fn transfer(old) { {left: old.left - 10, right: old.right + 10} }
atomic_update(state, transfer)
print(atomic_read(state))
atomic_close(state)
atomic_update synchronously computes and validates the candidate, then publishes it once. Errors, cancellation and quota failure before commit leave the old value intact.
The owner is the originating Runtime/PID. AtomicState cannot be sent across Workers, captured into another Worker or JSON-encoded. Ordinary variables have not become thread-safe, and cross-process shared memory has not been implemented by this API.
Candidate data is constrained to Null, Bool, Int, finite Float, String and recursive Lists or Maps. Functions, Streams and handles are excluded. That restriction gives publication and lifetime checks a manageable scope.
This is a small commit kernel I can validate. Multiple objects and multiple Workers will require a new memory model and recovery protocol, rather than a broader claim about the existing handle.
Files, databases and tasks do not form an automatic transaction
File updates are easy to misunderstand. Atomic replacement controls visibility. Avoiding lost updates when several writers read and modify a file also requires a common cooperative lock protocol.
atomic_file_update requires an existing regular file. Writers must cooperate through the same path and stable .hhy-lock file. Its Duration limits lock waiting only; it does not interrupt an arbitrarily long callback. Deleting the lock file while writers are active can change the identity of the lock they rely on.
Publication and durability remain separate. Writing and syncing a temporary file, replacing the target and syncing its parent directory each have failure points. HHY_PUBLISHED_DURABILITY_UNKNOWN means publication has occurred but directory durability is uncertain. Treating that error as proof that nothing was written can make a blind retry repeat a business action.
The database has a related problem. transaction_strict binds callback work to one transaction. A runtime error in that callback leaves it rollback-only even if the error is caught; swallowing the error cannot restore permission to commit. Strict callbacks cannot perform arbitrary file, HTTP or task effects either.
When a COMMIT response is lost, however, the client may receive DB_COMMIT_UNKNOWN. The database can have committed without the client receiving confirmation. Applications need a unique request key, result lookup and reconciliation before deciding what to do next. The language does not automatically provide exactly-once execution.
task_map takes a finite List, supervises bounded isolated tasks and returns results in input order. A later task's fast failure can be noticed without waiting for the first slow task to finish. On failure, direct children are terminated and collected together. External file, HTTP or database effects already completed by those children are not undone. Its result budget is not a global disk or RSS limit either.
I want callers to know which changes commit together and which recovery steps remain theirs. Runtime state, one file and a database form three independent atomic domains. Task supervision manages lifetimes. They do not automatically compose into a cross-resource transaction.
The baseline complicates the phrase “faster by default”
The September 14 report runs three existing benchmarks with both AST and Bytecode using the current 1.7.1 binary. Each mode has two warmups and seven measured samples, with alternating engine order, HHY_* experiment variables cleared, and matching output checked for every run.
Each sample starts a fresh process. Timing includes startup, parsing, compilation or verification, and execution. These are end-to-end measurements, rather than steady-state hot-loop timings.
| Workload | AST median | Bytecode median | BC / AST |
|---|---|---|---|
| core-flow, 100,000 items | 20.922 ms | 13.499 ms | 0.645× |
| json-flow, 500 conversions | 62.382 ms | 63.469 ms | 1.017× |
| call-closure, 100,000 captured-closure calls | 21.985 ms | 28.518 ms | 1.297× |
Bytecode takes about 35.5% less time on Flow, roughly similar time on JSON, and 29.7% more time on captured closures. All three produce equivalent output; their performance does not move in one direction.
I want to keep the closure row visible. The next useful step is to separate startup and steady-state cost with profiler, dispatch and lookup comparisons. This table alone cannot identify the specific lookup or dispatch mechanism responsible for the difference.
It is also not a v1.7.0 versus v1.7.1 comparison. It compares two engines in the same 1.7.1 binary. The development machine was not frequency-locked or CPU-pinned, background services remained active, and there were no multi-host repeats. The data gives me a starting point for investigation, not p99 latency, a production SLA or a ranking against other languages.
Development-time binding-cache and source-fusion records also show mixed results: small benefits in some workloads and regressions in workloads such as file distinct. Their comparison binary was saved before development rather than rebuilt cleanly from a tag. Dividing those absolute timings by this new table would not establish a release-to-release speedup.
Compiler, cache and fusion candidates therefore retain independent admission decisions based on real workloads, compatibility and resource budgets. An implementation alone is insufficient reason to enable one by default.
Keeping the unanswered questions beside the evidence
The report newly ran 69 engine/configuration cases and 80 contending cooperative file updates. The 69 cases include expected rejections: passing means behavior matches the contract, not that every input succeeds.
Archived evidence also includes database idempotency and unknown-commit recovery, a 100,000-request Web test, four-platform CI and release-package validation. The Web record reports 32 clients, no failures and roughly 2,391 requests per second. That is an observation from one machine and workload, not maximum capacity or database application throughput.
The 120-second soak with 9,360,000 owner commits belongs to an older development binary. It is neither a 24-hour soak of the current release nor a shared-memory concurrency test. Linux x86_64 also has a BDWGC/ASan limitation in the extremely low-memory unwind path; sanitizer coverage should not be described as identical on every platform.
Release measurement had visible variability too. The first macOS I/O/JSON ratio was 1.3496, above the 1.10 gate. A local check of the same commit returned 1.0032; rerunning the failed jobs passed without changing code or thresholds. Retaining that history is more useful than saying everything passed on the first attempt.
My next priorities are a 24-hour soak on the candidate build, faults before and after publication, and database invariants across isolation levels, deadlocks, duplicate requests and restart recovery. Web concurrency steps, tail latency and resource curves follow. Claims should expand with the evidence.
1.x: Stabilize existing APIs individually
The public roadmap groups the next work into three major series. I use that level here rather than turning every patch number in an internal planning draft into a promised release.
For 1.x, the focus is the existing seven APIs: fix ownership, effects, error categories and resource contracts, then validate the candidate with soak testing, fault injection, platform coverage and AST/Bytecode parity.
State, file, database and task capabilities should be judged separately. A mature capability can lose its experimental gate while another stays experimental.
Stable availability means an admitted API can be called without HHY_CONCURRENCY_EXPERIMENTS. Developers still explicitly request atomic updates or strict transactions. An upgrade will not silently turn ordinary variables into transactional state.
Performance optimization has a separate gate. HIR, caches or fusion still need end-to-end benefits after correctness passes, and may remain disabled indefinitely.
2.x: Design shared state and coroutines together
The 2.x goals are domain-wide state atomicity, atomic shared memory across Workers and a coroutine scheduler.
“Global” has a boundary here: managed shared objects inside a declared StateDomain participate in unified multi-object transactions. Arbitrary ordinary variables, files, HTTP calls and databases do not automatically join that transaction.
Shared memory cannot simply contain pointers into today's GC heap. It needs a shared arena, generation-checked handles, versions and read/write sets, a unified commit point, and recovery and reclamation rules after Worker crashes. Sharing among Workers on one host is distinct from consistency across nodes.
Coroutines require resumable execution frames that retain local values, exception handlers, resource scopes and GC roots, followed by nonblocking I/O and structured cancellation. Existing Stream or SSE support does not mean this kernel exists already.
I am particularly interested in the intersection: whether execution may suspend during atomic publication, who cleans a candidate when cancellation arrives, whether a waiting task can lose its wakeup, and who retains old versions. Independently tested components still require joint validation.
The new mode should begin experimentally and become stable after correctness, recovery, fairness and resource acceptance. Existing programs must retain their sharing and scheduling semantics unless their authors choose to migrate.
3.x: Make uncertain outcomes recoverable across nodes
The 3.x direction is distributed transactions: a durable coordinator, prepare/commit/rollback participants, decision logs, UNKNOWN queries and recovery, followed by high availability, fault injection and operational tooling.
I want to support participants with an explicit transaction protocol. An ordinary HTTP service without recoverable prepare and commit does not acquire atomic rollback merely by being called from HHY. Explicit compensation is useful, but it has a different guarantee.
Acceptance will focus on participant crashes, network partitions, duplicate messages, lost acknowledgements and how a restarted coordinator continues a recorded decision. Stable APIs will still require explicit databases, coordinator configuration and a supported-participant matrix.
From v1.7.0 to v1.7.1, my aim for HHY remains clear data flow and explainable system behavior. With every capability I add, I now ask one more question: after it fails, can the caller still determine what happened and finish the work?
References and related reading
- HHY 1.7.1 language status report: capabilities, measurements and evidence boundaries as of September 14, 2026.
- v1.7.1 release and release notes.
- Controlled concurrency and atomicity guide.
- Language and VM evolution roadmap: the public version grouping used for future directions here.
- From v1.5 to v1.7: Building a Verifiable Optimization Pipeline for HHY.
- HHY Database 1.0.