Repository navigation
Description
Motivation and scope
NLS needs a deterministic way for a typed operation to request storage, transport, or another service while allowing an enclosing program or test to supply the implementation. Osprey provides a concrete reference for typed operations, operation-specific lexical handlers, and test injection.
Build on the IR already delivered by #194 / #203 and the semantic passes in #195–#198. This is a new language feature; the illustrative node names below are a proposed design, not existing .nl syntax.
Minimum deliverable
Add typed operation declarations and direct-return lexical handlers. An operation request receives its declared result from the innermost matching arm, and execution continues normally. Implement this synchronous slice before adding resumable continuations.
- Model stable effect/operation identities, parameter and result TypeRefs, failure bounds, and resource contracts. Add
PerformandHandleregions to the shared IR; do not infer a request from narrative English. - Resolve each arm to an operation symbol and check argument arity/types, arm result type, and duplicate/unknown arms. Begin with monomorphic operations; generic effect instances require distinct resolved identities if introduced.
- A partial handler discharges only its supplied operations. Inner arms override matching operations; missing operations can fall through to an enclosing matching handler.
- Propagate required operations through actual calls. For supported callbacks, invocation contributes latent requirements; merely constructing a closure does not execute them. Reject unsupported dynamic provenance conservatively.
- An executable entry must discharge every requirement. A library may expose a checked requirement contract; a host adapter must validate its installation before execution. Writing an effect annotation neither installs a handler nor grants authority.
- A closure returned from a handled region does not retain an implicit permission to perform there after the region ends. Check requirements at later invocation. Make the behavior of performs inside handler arms explicit: initially reject recursive invocation of the active operation; allow different operations to resolve outward under documented rules.
- Preserve both the remaining operation requirements and the selected implementation's resource footprint and failures. A disk-backed Storage handler still writes files after Storage requirements are discharged. An in-memory handler has a different footprint; an unknown handler implementation is not pure.
Implementation hints
Extend nlsc/ir.py, nlsc/lowering.py, and the shared pipeline, reusing the symbol/type tables from #195 and summaries from #197/#198. Avoid a second prose parser inside either emitter.
An explicit scoped handler environment threaded through generated calls is a reasonable first lowering. It must restore the outer environment on normal return and typed failure, avoid global cross-request state, and reject unsupported escape/concurrency cases. This issue does not require native stack capture, first-class handler values, fibers, or an asynchronous runtime.
Specify the .nl spelling and migration separately from the IR contract. Keep PURPOSE/descriptions narrative and resolve every executable operation mechanically.
Acceptance criteria
- The same storage-using ANLU runs through a temporary-directory adapter and an in-memory adapter without changing its business logic; results and ordered operation traces match the declared contract.
- A refusing adapter produces a declared typed failure for a write it cannot perform; it cannot report successful persistence by silently returning a placeholder.
- Unknown operations, wrong argument/arm types, duplicate arms, missing handlers at entry, and an unhandled operation in a partial handler are rejected with source spans.
- Nested partial handlers select the innermost matching arm and retain the appropriate outer arms.
- A supported callback carries its latent requirements through wrappers; an escaping callable cannot inherit discharged requirements from its creation site.
- Handler failures propagate; handler installation is restored after failure; independent invocations cannot share handler state accidentally.
- Disk and memory implementations retain distinct effect summaries, and empty unhandled requirements never independently certify parallel safety.
- Python and TypeScript have matching direct-handler behavior, or the shared capability check explicitly rejects the feature for an unsupported target before emission.
Dependencies and boundaries
Depends on #195, #196, #197, and #198; #194 is already complete. Integrate with #202's backend harness. Explicit resumption and its consumption rules are tracked in #208; they do not block this direct-return slice. Existing typestate, retry, and scheduling work remains separately scoped.
Osprey implementation references
Inspected at 8792de9f6d159922141f9d191e4bc5d256fbb971; these are design/code references, not a claim that its runtime tests were executed in this audit.
- Typed operations, lexical lookup, partial discharge, and escaping callbacks.
- Operation requirements and callback/fixed-point analysis and checker tests.
- Disk, memory, and read-only injection fixture. Borrow the interchangeable implementation and trace-testing pattern; NLS should specify its own write-failure contract.
Activity
Mnehmos commentedon Sep 12, 2026
Staging note (planning pass, 2026-09-13)
Audited against current master. This remains the largest language feature on the roadmap and is not blocked on missing infrastructure so much as on sequencing: the checked-IR pipeline (lowering → typecheck/controlflow/failures/effects) and both IR-consuming emitters have landed, but every #207 acceptance criterion is behavioral (same ANLU across disk/memory adapters, ordered effect traces, refusing-adapter typed failures), so a checker-only slice would deliver syntax/IR freezing at best — not the issue's deliverable.
Slice 1 design, recorded so the work can start without re-deriving it:
@test/@literal): aHANDLE:ANLU section withON <operation> -> <expr>arms, attachable to any ANLU;PERFORM <op>(...) -> nameLOGIC steps._should_use_regex_canonical_parsemust gainHANDLE:/PERFORMalternatives, and_sanitize_source_for_treesittershould blank the new sections — the tree-sitter grammar cannot be rebuilt in this environment.HandleArm(op_path, expr, span)in the schema;IRPerform(op, args, result_name)statement plusIROperation.handlers: tuple[IRHandleArm, ...]following the existing optional-contract-slot pattern (effects/failures/typestate).ETARGET002capability entry, so nothing is silently dropped while the runtime is being designed.Rationale for keeping this open rather than closing: the issue's own refinements section already fixes the boundary (synchronous direct-return lexical handlers first; #208 owns resumption), so the design is recorded but the deliverable is not delivered. #208 depends on this landing first; it should not start earlier.