Skip to content

A2: @suspending decorator, host-resource method/static scope, receiver binding - #53

Merged
lann merged 2 commits into
mainfrom
suspending-decorator-scope
Aug 10, 2026
Merged

A2: @suspending decorator, host-resource method/static scope, receiver binding#53
lann merged 2 commits into
mainfrom
suspending-decorator-scope

Conversation

@lann

@lann lann commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Follow-on to #51 (A1), triggered by two questions from review discussion: can suspending be a decorator, and why isn't the canonical import shape a class?

The load-bearing part: A1 scoped marking to plain functions — but [method]pollable.block, the site the tier-(c) WASI blocking profile exists for, is a host-resource method. A2 extends the scope: instance methods are marked on the class (prototype = per-declaration brand authority, read at wrap time; instance overrides swap the body, never suspendability), statics on the function itself, constructors never (C2).

Decorator: suspending now works as a stage-3 method decorator (@suspending read() {…}) alongside the canonical call form. It refuses loudly rather than no-op: non-method kinds throw at class-definition time, and the legacy experimentalDecorators convention throws with guidance (it would receive the prototype — branding the wrong object and corrupting the descriptor). Note the A1 single-arg form already type-checked in decorator position by arity-assignability; without kind checks that was a compile-pass/silent-no-op trap.

Receiver binding (the class-provider defect): the plain dispatch arm called extracted interface members unbound while the static arm bound cls — so a class instance type-checked as an interface provider and broke with this === undefined on first state access. A2 rule: interface members get their containing object as receiver; bare world-level imports stay unbound. Records remain the canonical type; class instances become a properly supported spelling.

New fixture suspending-method.wat (smallest host resource with all three member forms — no existing testdata component imports a true [method]). 14 pins total in suspending_imports_test.ts.

Second commit: wasi-shims/random.ts comment scopes the exact-len claim to the 0.2 track and records wasi:random@0.3.0's short-read latitude (post-consolidation authority WebAssembly/WASI proposals/random; the archived repo's rc still shows exact-len). Behavior unchanged — chunk-to-full conforms on both tracks.

Gates: test-runtime 350/0; conformance 1254/0, 0 unexpected, 0 stale; sched-seeds, shells, wasi-shims/ct-runner/bundle/ports/webrtc green; smoke-tls PASS; websocket-conformance green (consumer tree verified clean); iroh-exam 4/5 (documented pre-existing scenario-3 environmental).

Next: Track 2 proper — the parking-pollable kernel (blocking profile) over this surface.

lann added 2 commits August 10, 2026 16:57
…r receiver binding

Three related extensions to A1, motivated by one observation: the tier-(c)
blocking profile's load-bearing site — [method]pollable.block — is a
host-resource METHOD, which A1's plain-functions-only scope did not cover.

- suspending() becomes dual-form: the direct call stays canonical (and is
  the only form possible in record literals), and it now works as a
  stage-3 method decorator on instance and static methods. The decorator
  REFUSES what it cannot mark, at class-definition time: non-method kinds
  (class/getter/setter/accessor/field) throw naming the kind, and the
  TypeScript-legacy experimentalDecorators calling convention throws with
  guidance — under it the decorator receives the prototype, and marking
  that would brand the wrong object and corrupt the property descriptor.
  (The single-arg A1 form already type-checked in decorator position by
  arity-assignability; blessing it without the kind checks would have
  traded a compile error for a silent no-op on resource classes.)

- A1 scope extends to host-resource methods and statics. The brand
  authority for an instance method is the CLASS PROTOTYPE, read at
  dispatcher-construction time — the Suspending-wrap decision is
  per-declaration and taken at instantiation, before any instance exists.
  Instance-level method overrides change the dispatched body, never
  suspendability. Statics carry the brand on the function itself.
  Constructors stay unmarkable (synchronous by the C2 amendment).
  New fixture suspending-method.wat: the smallest host resource with all
  three member forms ([constructor]/[method]/[static]) — no testdata
  component imports a true resource method (host-borrow/imported-resource
  use plain functions taking borrows).

- A2 receiver rule: interface members are invoked with their containing
  object as this, matching the resource static arm's apply(cls). The
  plain arm previously called extracted functions UNBOUND: a class
  instance type-checked as an interface provider, worked while stateless,
  and broke with this === undefined the moment a method touched instance
  state — the silent liberal-acceptance failure the contract forbids
  (and it would have made a parked @suspending method that reads its own
  state a cruel joke). World-level bare imports have no containing object
  and stay unbound.

contracts/embedder-api.md: Status gains A2; the A1 bullet's scope and
spelling rules updated.

Pins (tests/embedder/suspending_imports_test.ts, now 14): decorated
provider-class method parks with this bound; receiver binding alone
(unmarked, stateful, plain mode); decorated resource method parks
(probe(21) == 42 — the pollable.block shape) and decorated static parks,
with the guest-driven constructor staying synchronous; getter decoration
throws naming the kind; the legacy convention throws pointing at stage-3.

Gates: test-runtime 350/0; conformance 1254/0 (0 unexpected, 0 stale);
sched-seeds, shells, test-wasi-shims/ct-runner/bundle/ports/webrtc green;
smoke-tls suites PASS; websocket-conformance green (consumer tree clean
before/after); iroh-exam 4/5 with the documented pre-existing scenario-3
environmental.
The chunking rationale cited 'the WIT' as requiring exactly len bytes —
true for the @0.2 track this fragment serves, but wasi:random@0.3.0
(post-consolidation authority: WebAssembly/WASI proposals/random) renames
len to max-len and permits short reads, with callers required to loop and
a >=1-byte floor. Scope the exact-len claim to 0.2 and record where 0.3
differs, so a future @0.3 fragment inherits neither rule blindly — in
either direction. The archived wasi-random repo's 0.3 rc still carries
the old exact-len text; cite the moved authority. Behavior unchanged:
chunk-to-full conforms on both tracks.
@lann
lann enabled auto-merge August 10, 2026 20:58
@lann
lann merged commit d994ed0 into main Aug 10, 2026
4 checks passed
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