Skip to content

fix: register actors from imported modules in type checker - #2

Merged
slepp merged 1 commit into
mainfrom
fix/actors-in-modules
Feb 23, 2026
Merged

fix: register actors from imported modules in type checker#2
slepp merged 1 commit into
mainfrom
fix/actors-in-modules

Conversation

@slepp

@slepp slepp commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Three module registration functions (register_stdlib_hew_items, register_user_module, register_file_import_items) silently ignored Item::Actor via catch-all _ => {} arms — actors defined in imported modules were never registered
  • Adds Item::Actor handling to all three registration paths: type def, receive fn signatures, qualified aliases, and import spec support
  • Extends check_spawn to resolve module-qualified actor names (spawn module.ActorName(args)) via Expr::FieldAccess

Test plan

  • 5 new integration tests in module_system_test.rs covering bare, glob, and named actor imports
  • 2 new E2E .hew files (counter_actor_lib.hew + module_actor.hew) that type-check successfully
  • All 234 hew-types tests pass
  • Full workspace builds clean (cargo build --workspace)
  • cargo clippy -p hew-types --tests — no new warnings

Three module registration functions (register_stdlib_hew_items,
register_user_module, register_file_import_items) had catch-all
match arms that silently ignored Item::Actor. This meant actors
defined in imported modules were never registered — no type def,
no receive fn signatures, no qualified aliases.

Also extend check_spawn to resolve module-qualified actor names
(spawn module.ActorName(args)) via Expr::FieldAccess.

Adds 5 integration tests covering bare, glob, and named actor
imports plus E2E test files for module-level actor definitions.
@slepp
slepp merged commit cfbc451 into main Feb 23, 2026
2 of 3 checks passed
@slepp
slepp deleted the fix/actors-in-modules branch February 23, 2026 06:05
slepp added a commit that referenced this pull request Mar 31, 2026
… current-actor slot

Two verified bugs in the WASM scheduler were causing incorrect behaviour:

Bug #1 - self APIs see null/0 during WASM dispatch
  scheduler_wasm::activate_actor_wasm wrote its own private
  CURRENT_ACTOR static, while actor.rs self APIs
  (hew_actor_self, hew_actor_self_pid, hew_actor_self_stop) read
  CURRENT_ACTOR_WASM via set_current_actor - a different location.
  Fix: call crate::actor::set_current_actor() instead, which writes
  the canonical slot actor.rs reads on both wasm32 and native.

Bug #2 - nested/reentrant activation overwrites and clears outer state
  ACTIVATING, CURRENT_ACTOR, PREV_ARENA, CURRENT_REPLY_CHANNEL were
  single-slot globals. Nested activation (hew_actor_ask ->
  hew_sched_run -> activate_actor_wasm) overwrote and then zeroed them,
  leaving the outer dispatch with null current actor and cleared state.
  Fix: save all per-activation globals at entry and restore on exit.
  The private CURRENT_ACTOR static is removed.

Tests added: self_api_sees_current_actor_during_dispatch and
nested_activation_preserves_outer_actor in scheduler_wasm::tests.

All 903 hew-runtime unit tests pass; clippy clean.
slepp added a commit that referenced this pull request Mar 31, 2026
… current-actor slot (#430)

Two verified bugs in the WASM scheduler were causing incorrect behaviour:

Bug #1 - self APIs see null/0 during WASM dispatch
  scheduler_wasm::activate_actor_wasm wrote its own private
  CURRENT_ACTOR static, while actor.rs self APIs
  (hew_actor_self, hew_actor_self_pid, hew_actor_self_stop) read
  CURRENT_ACTOR_WASM via set_current_actor - a different location.
  Fix: call crate::actor::set_current_actor() instead, which writes
  the canonical slot actor.rs reads on both wasm32 and native.

Bug #2 - nested/reentrant activation overwrites and clears outer state
  ACTIVATING, CURRENT_ACTOR, PREV_ARENA, CURRENT_REPLY_CHANNEL were
  single-slot globals. Nested activation (hew_actor_ask ->
  hew_sched_run -> activate_actor_wasm) overwrote and then zeroed them,
  leaving the outer dispatch with null current actor and cleared state.
  Fix: save all per-activation globals at entry and restore on exit.
  The private CURRENT_ACTOR static is removed.

Tests added: self_api_sees_current_actor_during_dispatch and
nested_activation_preserves_outer_actor in scheduler_wasm::tests.

All 903 hew-runtime unit tests pass; clippy clean.
slepp added a commit that referenced this pull request Apr 11, 2026
The test is timing-sensitive on Windows CI: it polls hew_supervisor_get_child
in the second crash iteration, but on a heavily-loaded runner the scheduler
can take longer than the 5-second window to process the supervisor restart
after crash #1.  When that happens the inner poll exits via timeout and crash
#2 is never injected, leaving the crash log at 1 entry instead of 2.

This is the same class of jitter already covered by the
connection_drop_wakes_pending_remote_ask retry.  One retry absorbs a single
transient hiccup without hiding real circuit-breaker regressions.

Observed: run 24272197851, job 70879288506 (Windows) — 12.09s, before=0 after=1.
Baseline on main: PASS 0.109s (run 24271597476, job 70877552018).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
slepp added a commit that referenced this pull request Apr 11, 2026
* runtime: fix two race-window bugs in hew_node_stop/node_inbound_router

Fix 1 – spawn-window close (node_inbound_router):
After INBOUND_ASK_ACTIVE is incremented, check shutdown_started
before spawning a worker thread.  If the flag is already set the
counter is decremented and the function returns without spawning,
so no new workers are created once node teardown has marked the
connection manager as stopping.

Fix 2 – per-conn_mgr drain (hew_node_stop):
Replacing the old global-INBOUND_ASK_ACTIVE drain (which caused
node_stop_wakes_pending_remote_ask to wait for workers on a different
node in multi-node tests) with a per-conn_mgr AtomicUsize.

Added field HewConnMgr.inbound_ask_active (Arc<AtomicUsize>) and
accessor hew_connmgr_inbound_ask_active().  InboundAskGuard is now a
tuple struct carrying an Arc clone of the per-manager counter; its
Drop impl decrements the global first, then the per-manager counter
(the drain watches per-manager, so global is always decremented before
per-manager reaches zero – no false-idle window).

hew_node_stop captures the Arc before hew_connmgr_free, then spins
(≤ 5 s) on the per-manager counter after the free returns.  Because
hew_connmgr_free joins the reader threads first, no new router calls
can happen after the free; the drain therefore terminates as soon as
all already-running workers exit.

Regression tests added:
* inbound_router_no_spawn_after_shutdown_started – marks conn_mgr
  stopping, calls the router directly, asserts neither the global nor
  the per-manager counter changes.
* node_stop_drains_inbound_ask_active – artificially inflates both
  counters, has a background thread decrement them after 60 ms, and
  asserts that hew_node_stop waited (elapsed ≥ 40 ms) for the drain.

Updated two existing guard unit tests (inbound_ask_guard_decrements_on_drop,
inbound_ask_guard_pair_decrements_twice) to use the new tuple-struct
signature.

* ci(nextest): add retry for circuit_breaker_trips_on_repeated_crashes

The test is timing-sensitive on Windows CI: it polls hew_supervisor_get_child
in the second crash iteration, but on a heavily-loaded runner the scheduler
can take longer than the 5-second window to process the supervisor restart
after crash #1.  When that happens the inner poll exits via timeout and crash
#2 is never injected, leaving the crash log at 1 entry instead of 2.

This is the same class of jitter already covered by the
connection_drop_wakes_pending_remote_ask retry.  One retry absorbs a single
transient hiccup without hiding real circuit-breaker regressions.

Observed: run 24272197851, job 70879288506 (Windows) — 12.09s, before=0 after=1.
Baseline on main: PASS 0.109s (run 24271597476, job 70877552018).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
slepp added a commit that referenced this pull request Jul 27, 2026
…t probes

The oracle was wrong, not the answer. `hew_tls_read_result` DOES transfer its
buffer; `result-retention = "transferred"` and the mint it licenses stand
unchanged. What was broken was the instrument.

MEASURED ON LINUX, not inferred from macOS. Ubuntu 26.04, glibc 2.43.

  before: SIGABRT [1.522s] hew-std tls::tests::read_result_transfers_the_decrypted_buffer
          tcache_thread_shutdown(): unaligned tcache chunk detected
  after:  PASS    [0.406s] hew-std tls::tests::read_result_transfers_the_decrypted_buffer

THE CORRUPTING ACCESS, named. Valgrind on the same binary:

  Invalid read of size 4
    at core::sync::atomic::Atomic<u32>::fetch_sub
    by hew_bytes_drop (bytes.rs:319)
    by read_result_transfers_the_decrypted_buffer (tls.rs:1882)   <- R3
  Address 0x6821360 is 0 bytes inside a block of size 24 free'd
    by hew_bytes_drop (bytes.rs:325)
    by read_result_transfers_the_decrypted_buffer (tls.rs:1875)   <- R2 counterfactual
  Block was alloc'd at
    by alloc_buf (bytes.rs:151) <- hew_bytes_from_static <- read_tls_vec
    <- hew_tls_read <- hew_tls_read_result

`fetch_sub` is a read-modify-WRITE, so R3's release stores 0xFFFFFFFF over the
first four bytes of a chunk already on the tcache free list — the low half of
its `next` link. glibc walks that list at thread exit and finds the link
unaligned. macOS never noticed because libmalloc does not police a per-thread
free list whose link lives at offset 0 of the freed block.

WHY THE BLOCK WAS ALREADY FREE. Refcount trace of the R2 counterfactual,
instrumented on the same host at the real allocation:

  alloc                 rc=1
  probe #1  -> true     rc=1
  clone_ref             rc=2
  probe #2  -> false    rc=1   <- the probe consumed the counterfactual's ref
  tls.rs:1875 drop      rc=0   <- FREED here, one release early
  tls.rs:1882 drop             <- the write above, into the freed chunk

`bytes_owner_is_sole` pushes into a COPY of the triple to keep the caller's
triple aimed at the original. On the shared reading `hew_bytes_push` reaches
`ensure_unique`, which forks a private buffer and RELEASES the pusher's
reference to the original — correct for an ordinary caller, which is moving its
value forward onto the fork, and wrong for a probe that deliberately kept a
second triple on the original. The probe dropped the fork but never put that
reference back, so the counterfactual's `clone_ref` was cancelled by the probe
rather than by the release written to balance it.

THE FIX. The probe is now owner-count neutral: on the fork path it retains the
original before releasing the fork, so both allocations leave with the count
they arrived with. Its premise is also asserted properly — `offset == 0` as
well as `len < MIN_CAPACITY`, since "a push cannot grow this" is a fact about
`offset + len` against capacity, not about `len` alone.

IT STILL DISCRIMINATES. Verified by mutation on Linux: with `read_tls_vec`
patched to retain the buffer it returns, the fixed probe fails as intended —

  panicked at hew-std/src/tls.rs:1874:
  the first result must be solely owned by the caller

and the counterfactual inside the passing test still reports NOT sole with a
second reference outstanding. The `result-retention` row keeps its teeth.

THE TRAP IS PINNED WHERE IT LIVES. `hew_bytes_push` and `ensure_unique` now
state that the copy-on-write path consumes the caller's reference, and
`bytes::tests::cow_push_consumes_the_pushers_reference_to_the_original` asserts
it against the refcount word so the next probe author reads it as a fact rather
than deriving it from the implementation.

Valgrind after: zero invalid accesses, zero definitely-lost. The four remaining
contexts are pre-existing rustls/ring uninitialised-padding reports, present
before this change. Full hew-std + hew-runtime on Linux: 3391/3391 pass.
slepp added a commit that referenced this pull request Jul 27, 2026
…t probes

The oracle was wrong, not the answer. `hew_tls_read_result` DOES transfer its
buffer; `result-retention = "transferred"` and the mint it licenses stand
unchanged. What was broken was the instrument.

MEASURED ON LINUX, not inferred from macOS. Ubuntu 26.04, glibc 2.43.

  before: SIGABRT [1.522s] hew-std tls::tests::read_result_transfers_the_decrypted_buffer
          tcache_thread_shutdown(): unaligned tcache chunk detected
  after:  PASS    [0.406s] hew-std tls::tests::read_result_transfers_the_decrypted_buffer

THE CORRUPTING ACCESS, named. Valgrind on the same binary:

  Invalid read of size 4
    at core::sync::atomic::Atomic<u32>::fetch_sub
    by hew_bytes_drop (bytes.rs:319)
    by read_result_transfers_the_decrypted_buffer (tls.rs:1882)   <- R3
  Address 0x6821360 is 0 bytes inside a block of size 24 free'd
    by hew_bytes_drop (bytes.rs:325)
    by read_result_transfers_the_decrypted_buffer (tls.rs:1875)   <- R2 counterfactual
  Block was alloc'd at
    by alloc_buf (bytes.rs:151) <- hew_bytes_from_static <- read_tls_vec
    <- hew_tls_read <- hew_tls_read_result

`fetch_sub` is a read-modify-WRITE, so R3's release stores 0xFFFFFFFF over the
first four bytes of a chunk already on the tcache free list — the low half of
its `next` link. glibc walks that list at thread exit and finds the link
unaligned. macOS never noticed because libmalloc does not police a per-thread
free list whose link lives at offset 0 of the freed block.

WHY THE BLOCK WAS ALREADY FREE. Refcount trace of the R2 counterfactual,
instrumented on the same host at the real allocation:

  alloc                 rc=1
  probe #1  -> true     rc=1
  clone_ref             rc=2
  probe #2  -> false    rc=1   <- the probe consumed the counterfactual's ref
  tls.rs:1875 drop      rc=0   <- FREED here, one release early
  tls.rs:1882 drop             <- the write above, into the freed chunk

`bytes_owner_is_sole` pushes into a COPY of the triple to keep the caller's
triple aimed at the original. On the shared reading `hew_bytes_push` reaches
`ensure_unique`, which forks a private buffer and RELEASES the pusher's
reference to the original — correct for an ordinary caller, which is moving its
value forward onto the fork, and wrong for a probe that deliberately kept a
second triple on the original. The probe dropped the fork but never put that
reference back, so the counterfactual's `clone_ref` was cancelled by the probe
rather than by the release written to balance it.

THE FIX. The probe is now owner-count neutral: on the fork path it retains the
original before releasing the fork, so both allocations leave with the count
they arrived with. Its premise is also asserted properly — `offset == 0` as
well as `len < MIN_CAPACITY`, since "a push cannot grow this" is a fact about
`offset + len` against capacity, not about `len` alone.

IT STILL DISCRIMINATES. Verified by mutation on Linux: with `read_tls_vec`
patched to retain the buffer it returns, the fixed probe fails as intended —

  panicked at hew-std/src/tls.rs:1874:
  the first result must be solely owned by the caller

and the counterfactual inside the passing test still reports NOT sole with a
second reference outstanding. The `result-retention` row keeps its teeth.

THE TRAP IS PINNED WHERE IT LIVES. `hew_bytes_push` and `ensure_unique` now
state that the copy-on-write path consumes the caller's reference, and
`bytes::tests::cow_push_consumes_the_pushers_reference_to_the_original` asserts
it against the refcount word so the next probe author reads it as a fact rather
than deriving it from the implementation.

Valgrind after: zero invalid accesses, zero definitely-lost. The four remaining
contexts are pre-existing rustls/ring uninitialised-padding reports, present
before this change. Full hew-std + hew-runtime on Linux: 3391/3391 pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant