Repository navigation
Description
Problem
Retrying a requested operation and replaying the rest of its caller are different actions. A payment handler may make two permitted gateway attempts yet must deliver only one successful answer to the workflow that sends the receipt.
#207 introduces direct-return handlers. This follow-up adds a small, explicit resumption contract to the typed control IR. It does not claim exactly-once external payment execution; replay/idempotency obligations remain in #201, and resource-token consumption remains in #200.
Minimum deliverable
Support a synchronous affine, single-shot request continuation: it may be resumed at most once. The first slice uses an explicit resuming-arm mode, with terminal Resume(value) or a declared typed failure that exits the handled region. These names are proposed IR constructs, not existing .nl syntax.
Keep direct-return arms from #207 semantically distinct. Do not infer their behavior from whether a sibling arm contains the word resume. Defer successful zero-resume answer replacement, work after resume returns, multi-shot continuations, first-class continuation values, and cross-fiber resumption until their control and cleanup contracts have separate implementations.
Implementation hints
- Give each dynamic operation request its own continuation token and region identity. It moves from pending to resumed or failed/closed. This is per request, not one token per operation declaration or whole program.
Resume(value)checks the operation's result type. The surrounding handled region retains its own answer type; those types need not match. A handler failure follows feat(errors): add checked failure sets and a portable typed exception contract #198's checked failure contract and produces no successful operation result.- Extend bug(control): check branch outputs, binding mutation, and loop-carried values before emission #196's control analysis with token use counts: sum along sequential paths and take the maximum across mutually exclusive alternatives. Conservatively reject a loop or recursive/indirect path that can reuse the same token. Two resume sites on exclusive branches are legal.
- Initially keep the token lexical and non-first-class: reject copying, storage in records, returning it, passing it to unknown callbacks, or crossing task/fiber boundaries. Track aliases if any safe forwarding form is later allowed.
- Specify that the same lexical handlers remain installed for the resumed remainder. Preserve ordered effects, typed failure propagation, and cleanup when the region exits.
- Lower through an explicit continuation/state-machine or equivalent structured protocol shared by backend contracts. Do not require Python/TypeScript to capture arbitrary native stacks. Gate unsupported forms before output under feat(backends): consume checked IR and enforce executable Python/TypeScript semantic conformance #202.
- Reta 8000 in a runtime consumed-token guard at foreign/runtime boundaries. A duplicate completion must not execute the downstream continuation twice, even if an adapter violates its declared contract.
- An operation's replayability, its continuation's consumption, and its resource's typestate are separate facts. None substitutes for the other two.
Acceptance criteria
- Sequential double resume, possible loop reuse, an escaped token, and a wrong resume value type are rejected with diagnostics pointing to the relevant operation and control paths.
- Exclusive success branches may each contain one resume; ordinary successive operation requests receive distinct tokens.
- A fake gateway fixture records two allowed internal attempts, one resume, and one receipt in that order. The second attempt uses the same key/payload under feat(control): add checked Retry and Timeout policies with explicit idempotency obligations #201's policy when retry integration lands.
- A terminal declared decline performs zero resumes and sends zero receipts; its typed failure leaves the region and runs the supported cleanup exactly once.
- A runtime duplicate-completion probe cannot send a second receipt; cross-thread/asynchronous support remains unavailable until explicitly implemented.
- Nested handlers and region answer types preserve the same values, failure payloads, and ordered traces on each supporting backend.
- Documentation states the at-most-once downstream guarantee and explicitly separates it from external deduplication, cancellation, and exactly-once execution.
Ordering
Depends on #207, #196, #197, and #198. Use #202's conformance/capability mechanisms. #201 can implement ordinary bounded retry independently; handler-based retry integration follows this issue. No general concurrency runtime or complete ownership system is required.
Osprey references and limits
Pinned to 8792de9f6d159922141f9d191e4bc5d256fbb971:
- Resuming handler semantics and per-arm mode.
- Multiplicity checker: useful reference for path-sensitive
oncechecks; the current implementation explicitly rejects unsupportedmanyand declaration-levelabortoperations. - Two attempts / one receipt and decline / zero receipts fixtures. The NLS first slice should express decline through its existing typed-exception contract, rather than copy Osprey's answer-replacing branch semantics without implementing them.
The reference tests were inspected, not executed during this audit. Osprey's single-shot design is an implementation reference, not evidence of externally exactly-once I/O.
Activity
Mnehmos commentedon Sep 12, 2026
Staging note (planning pass, 2026-09-13)
Depends on #207 by the issue's own ordering ("Use #207's operation handler boundary and #208's consumed-token checks when implementing handler-based retry"). Additional prerequisites, checked against master:
nlsc/controlflow.pyprovides only totality/immutability checks, not per-path token state.No code exists for handler resumption (no Resume/Perform nodes;
IROperationhas no handler slot). Keeping open with this note; the natural sequence is #207 slice 1 (syntax + IR + checker + capability refusal) → #207 runtime → #208 specification and checker.